//+------------------------------------------------------------------+
//| NewsFilter_Advanced.mqh                                          |
//| Economic-calendar entry filter + position flatten for MQL5       |
//| Version 1.0                                                      |
//+------------------------------------------------------------------+
//
// Everything NewsFilter_Simple.mqh does, plus the action layer:
//
//   NFA_EntryAllowed(symbol)   gate your entries (decision)
//   NFA_Manage(magic)          close positions and cancel pendings (action)
//
// The simple version answers "should I?" and leaves acting to you. This
// version acts, scoped to one magic number so it can never touch another
// EA's trades or a manual position.
//
// Windows:
//   entry block   [T - NFA_BlockMinutesBefore, T + NFA_BlockMinutesAfter]
//   flatten       [T - NFA_FlattenLeadMinutes, T)   stops at T
//   pending cancel  same as the entry block window
//
// The flatten window deliberately stops at T. Closing into the blown spread
// straight after a print is usually worse than letting your own stop handle
// it, so the sweep does not chase a position once the number is out.
//
// The flatten lead is clamped to the entry-block window. A lead longer than
// the block would close a position while entries are still allowed, and the
// EA would immediately re-enter: a churn loop that pays spread both ways.
//
// FAIL-OPEN by default, and the flatten sweep NEVER runs on a stale cache.
// Blocking entries on bad data costs you an opportunity. Closing positions
// on bad data costs you money, so that path requires a good cache always.
//
// Data source: the MT5 built-in economic calendar, cached in memory.
//
#property strict

#ifndef NEWS_FILTER_ADVANCED_MQH
#define NEWS_FILTER_ADVANCED_MQH

input string NFA_Header             = "----------- News Filter -----------";
input bool   NFA_UseNewsFilter      = false;  // Enable the filter
input int    NFA_BlockMinutesBefore = 30;     // Block entries N minutes before release
input int    NFA_BlockMinutesAfter  = 15;     // Block entries N minutes after release
input bool   NFA_FailClosed         = false;  // Calendar down: true = block all, false = trade on

input string NFA_ActionHeader       = "----------- News Filter: Actions -----------";
input bool   NFA_FlattenPositions   = false;  // Close open positions before a release
input int    NFA_FlattenLeadMinutes = 10;     // Start closing N minutes before (clamped to block window)
input bool   NFA_CancelPendings     = true;   // Cancel resting pending orders in the block window
input int    NFA_SlippagePoints     = 50;     // Deviation for the flatten close

input string NFA_FilterHeader       = "----------- News Filter: What Counts -----------";
input string NFA_Currencies         = "USD,EUR,GBP,JPY,CHF,CAD,AUD,NZD,CNY";
input string NFA_ExcludeEvents      = "Crude Oil,Natural Gas,Gasoline,Distillate,Rig Count";
input string NFA_SymbolOverrides    = "NDX=USD,SP500=USD,US30=USD,GER40=EUR,UK100=GBP";

input string NFA_LogHeader          = "----------- News Filter: Logging -----------";
input bool   NFA_LogBlocks          = true;   // Log each blocked entry (throttled)

#define NFA_REFRESH_SECONDS    14400
#define NFA_RETRY_SECONDS      60
#define NFA_LOOKAHEAD_SECONDS  259200
#define NFA_LOG_THROTTLE       60
#define NFA_MAX_CACHED_SYMBOLS 64

struct NFACalendarEvent
{
   datetime time;
   string   currency;
   string   name;
};

NFACalendarEvent nfaEvents[];
bool     nfaStale          = true;
datetime nfaNextRefresh    = 0;
datetime nfaLastWarnTime   = 0;
datetime nfaLastBlockLog   = 0;
datetime nfaLastCloseFail  = 0;

string   nfaSymbolNames[NFA_MAX_CACHED_SYMBOLS];
string   nfaSymbolCcys[NFA_MAX_CACHED_SYMBOLS];
int      nfaSymbolCount    = 0;

//+------------------------------------------------------------------+
//| Small helpers                                                     |
//+------------------------------------------------------------------+

bool NFA_ListContains(const string haystack, const string needle)
{
   if (needle == "" || haystack == "")
      return false;
   return StringFind("," + haystack + ",", "," + needle + ",") >= 0;
}

// True when the event name contains any term from NFA_ExcludeEvents.
// This is how you stop sector-specific releases (oil inventories, gas
// storage) blocking instruments that have nothing to do with them.
bool NFA_EventExcluded(const string event_name)
{
   if (NFA_ExcludeEvents == "")
      return false;

   string upper_name = event_name;
   StringToUpper(upper_name);

   string parts[];
   int count = StringSplit(NFA_ExcludeEvents, StringGetCharacter(",", 0), parts);
   for (int i = 0; i < count; i++)
   {
      string term = parts[i];
      StringTrimLeft(term);
      StringTrimRight(term);
      if (term == "")
         continue;
      StringToUpper(term);
      if (StringFind(upper_name, term) >= 0)
         return true;
   }
   return false;
}

// Manual symbol-to-currency map. Format: "NDX=USD,GER40=EUR".
// Edit NFA_SymbolOverrides to match your broker's instrument names.
string NFA_SymbolOverride(const string symbol)
{
   if (NFA_SymbolOverrides == "")
      return "";

   string pairs[];
   int count = StringSplit(NFA_SymbolOverrides, StringGetCharacter(",", 0), pairs);
   for (int i = 0; i < count; i++)
   {
      string kv[];
      if (StringSplit(pairs[i], StringGetCharacter("=", 0), kv) != 2)
         continue;
      StringTrimLeft(kv[0]);
      StringTrimRight(kv[0]);
      StringTrimLeft(kv[1]);
      StringTrimRight(kv[1]);
      if (kv[0] == symbol)
         return kv[1];
   }
   return "";
}

void NFA_Warn(const string where)
{
   datetime now = TimeCurrent();
   if (nfaLastWarnTime != 0 && now - nfaLastWarnTime < NFA_LOG_THROTTLE)
      return;
   nfaLastWarnTime = now;
   Print("[NewsFilter] WARNING: economic calendar unavailable (", where,
         "), error=", GetLastError(), " - entry gates are ",
         (NFA_FailClosed ? "BLOCKING (fail-closed)" : "open (fail-open)"),
         ", flatten sweep is disabled until the cache recovers");
}

// Broker-aware filling mode. Tested as a BITMASK, not with equality: a
// broker offering IOC|BOC but not FOK would otherwise get FOK and reject
// every order with retcode 10030.
ENUM_ORDER_TYPE_FILLING NFA_FillingMode(const string symbol)
{
   long modes = 0;
   if (!SymbolInfoInteger(symbol, SYMBOL_FILLING_MODE, modes))
      return ORDER_FILLING_RETURN;

   if ((modes & SYMBOL_FILLING_FOK) != 0)
      return ORDER_FILLING_FOK;
   if ((modes & SYMBOL_FILLING_IOC) != 0)
      return ORDER_FILLING_IOC;
   return ORDER_FILLING_RETURN;
}

bool NFA_RetcodeOK(const uint retcode)
{
   return retcode == TRADE_RETCODE_DONE
       || retcode == TRADE_RETCODE_DONE_PARTIAL
       || retcode == TRADE_RETCODE_PLACED;
}

//+------------------------------------------------------------------+
//| Event cache                                                       |
//+------------------------------------------------------------------+

void NFA_Refresh()
{
   if (!NFA_UseNewsFilter)
      return;

   datetime now = TimeCurrent();
   if (now < nfaNextRefresh)
      return;

   MqlCalendarValue values[];
   datetime from = now - (datetime)(NFA_BlockMinutesAfter * 60 + 3600);
   datetime to   = now + (datetime)NFA_LOOKAHEAD_SECONDS;

   ResetLastError();
   if (!CalendarValueHistory(values, from, to) || ArraySize(values) == 0)
   {
      nfaStale = true;
      nfaNextRefresh = now + NFA_RETRY_SECONDS;
      NFA_Warn("CalendarValueHistory");
      return;
   }

   ArrayResize(nfaEvents, 0);
   int excluded = 0;

   for (int i = 0; i < ArraySize(values); i++)
   {
      MqlCalendarEvent event;
      if (!CalendarEventById(values[i].event_id, event))
         continue;

      if (event.importance != CALENDAR_IMPORTANCE_HIGH)
         continue;

      MqlCalendarCountry country;
      if (!CalendarCountryById(event.country_id, country))
         continue;

      if (!NFA_ListContains(NFA_Currencies, country.currency))
         continue;

      if (NFA_EventExcluded(event.name))
      {
         excluded++;
         continue;
      }

      int n = ArraySize(nfaEvents);
      ArrayResize(nfaEvents, n + 1);
      nfaEvents[n].time     = values[i].time;
      nfaEvents[n].currency = country.currency;
      nfaEvents[n].name     = event.name;
   }

   nfaStale = false;
   nfaNextRefresh = now + NFA_REFRESH_SECONDS;

   // This log line is the only reliable preview of what the filter will act
   // on. The MT5 Calendar tab does not always display everything the
   // calendar API returns, so do not use the tab to predict behaviour here.
   string preview = "";
   for (int i = 0; i < ArraySize(nfaEvents) && i < 5; i++)
      preview += " | " + TimeToString(nfaEvents[i].time, TIME_DATE|TIME_MINUTES)
               + " " + nfaEvents[i].currency + " " + nfaEvents[i].name;

   Print("[NewsFilter] cached ", ArraySize(nfaEvents), " event(s) (raw=",
         ArraySize(values), ", name-excluded=", excluded, ")", preview);
}

//+------------------------------------------------------------------+
//| Symbol to currency mapping                                        |
//+------------------------------------------------------------------+

string NFA_SymbolCurrencies(const string symbol)
{
   for (int i = 0; i < nfaSymbolCount; i++)
      if (nfaSymbolNames[i] == symbol)
         return nfaSymbolCcys[i];

   string base = "";
   string profit = "";
   bool base_ok   = SymbolInfoString(symbol, SYMBOL_CURRENCY_BASE, base);
   bool profit_ok = SymbolInfoString(symbol, SYMBOL_CURRENCY_PROFIT, profit);

   // A failed lookup is not the same as an empty result. Caching a failure
   // would leave this symbol unprotected for the life of the EA, so retry.
   if (!base_ok && !profit_ok)
   {
      Print("[NewsFilter] WARNING: currency lookup failed for ", symbol, ", will retry");
      return "";
   }

   string result = "";
   if (NFA_ListContains(NFA_Currencies, base))
      result = base;
   if (profit != base && NFA_ListContains(NFA_Currencies, profit))
      result = (result == "" ? profit : result + "," + profit);

   string mapped = NFA_SymbolOverride(symbol);
   if (mapped != "" && !NFA_ListContains(result, mapped))
      result = (result == "" ? mapped : result + "," + mapped);

   if (nfaSymbolCount < NFA_MAX_CACHED_SYMBOLS)
   {
      nfaSymbolNames[nfaSymbolCount] = symbol;
      nfaSymbolCcys[nfaSymbolCount]  = result;
      nfaSymbolCount++;
   }

   Print("[NewsFilter] watching ", symbol, " -> ",
         (result == "" ? "NONE (no watched currency derived)" : result));
   return result;
}

//+------------------------------------------------------------------+
//| Window test                                                       |
//+------------------------------------------------------------------+

// ON TIME: TimeCurrent() and MqlCalendarValue.time are both SERVER time, so
// this comparison is internally consistent whatever your broker's offset.
// Your terminal's Experts-tab log timestamps are LOCAL time, a different
// clock. Convert before concluding a window fired at the wrong moment.
bool NFA_WindowActive(const string symbol, const int pre_seconds,
                      const int post_seconds, string &event_desc)
{
   string currencies = NFA_SymbolCurrencies(symbol);
   if (currencies == "")
      return false;

   datetime now = TimeCurrent();
   for (int i = 0; i < ArraySize(nfaEvents); i++)
   {
      if (!NFA_ListContains(currencies, nfaEvents[i].currency))
         continue;
      if (now < nfaEvents[i].time - pre_seconds || now > nfaEvents[i].time + post_seconds)
         continue;

      event_desc = nfaEvents[i].currency + " " + nfaEvents[i].name + " @ "
                 + TimeToString(nfaEvents[i].time, TIME_DATE|TIME_MINUTES);
      return true;
   }
   return false;
}

bool NFA_FlattenActive(const string symbol, string &event_desc)
{
   if (!NFA_UseNewsFilter || !NFA_FlattenPositions || nfaStale)
      return false;

   int lead = (int)MathMin(NFA_FlattenLeadMinutes, NFA_BlockMinutesBefore);
   return NFA_WindowActive(symbol, lead * 60, -1, event_desc);
}

bool NFA_CancelActive(const string symbol, string &event_desc)
{
   if (!NFA_UseNewsFilter || !NFA_CancelPendings || nfaStale)
      return false;

   return NFA_WindowActive(symbol, NFA_BlockMinutesBefore * 60,
                           NFA_BlockMinutesAfter * 60, event_desc);
}

//+------------------------------------------------------------------+
//| Public API                                                        |
//+------------------------------------------------------------------+

// Call once from OnInit().
void NFA_Init()
{
   nfaSymbolCount   = 0;
   nfaStale         = true;
   nfaNextRefresh   = 0;
   nfaLastWarnTime  = 0;
   nfaLastBlockLog  = 0;
   nfaLastCloseFail = 0;

   if (!NFA_UseNewsFilter)
      return;

   NFA_Refresh();

   Print("[NewsFilter] enabled: block -", NFA_BlockMinutesBefore, "m/+",
         NFA_BlockMinutesAfter, "m, flatten=", (NFA_FlattenPositions ? "on" : "off"),
         " (lead ", NFA_FlattenLeadMinutes, "m), cancelPendings=",
         (NFA_CancelPendings ? "on" : "off"),
         ", onOutage=", (NFA_FailClosed ? "BLOCK" : "trade"));

   if (NFA_FlattenPositions && NFA_FlattenLeadMinutes > NFA_BlockMinutesBefore)
      Print("[NewsFilter] WARNING: FlattenLeadMinutes (", NFA_FlattenLeadMinutes,
            ") exceeds BlockMinutesBefore (", NFA_BlockMinutesBefore,
            ") - clamped to the block window to avoid a close/re-enter loop");
}

// The entry gate. Call this at EVERY point your EA sends an entry order.
// One missed call site leaves that path unprotected, and it will not be
// obvious from the logs that it happened.
bool NFA_EntryAllowed(const string symbol)
{
   if (!NFA_UseNewsFilter)
      return true;

   NFA_Refresh();

   if (nfaStale)
   {
      NFA_Warn("EntryAllowed");
      return !NFA_FailClosed;
   }

   string event_desc = "";
   if (!NFA_WindowActive(symbol, NFA_BlockMinutesBefore * 60,
                         NFA_BlockMinutesAfter * 60, event_desc))
      return true;

   if (NFA_LogBlocks && TimeCurrent() - nfaLastBlockLog >= NFA_LOG_THROTTLE)
   {
      nfaLastBlockLog = TimeCurrent();
      Print("[NewsFilter] entry blocked ", symbol, ": ", event_desc);
   }
   return false;
}

// Close one position at market. Failures are not retried here: NFA_Manage
// re-runs every tick while the window is open, and that is the retry.
bool NFA_ClosePosition(const ulong ticket, const string event_desc)
{
   if (!PositionSelectByTicket(ticket))
      return true;   // already gone

   string symbol = PositionGetString(POSITION_SYMBOL);
   ENUM_POSITION_TYPE ptype = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

   MqlTradeRequest request;
   MqlTradeResult  result;
   ZeroMemory(request);
   ZeroMemory(result);

   request.action       = TRADE_ACTION_DEAL;
   request.position     = ticket;
   request.symbol       = symbol;
   request.magic        = (ulong)PositionGetInteger(POSITION_MAGIC);
   request.volume       = PositionGetDouble(POSITION_VOLUME);
   request.type         = (ptype == POSITION_TYPE_BUY ? ORDER_TYPE_SELL : ORDER_TYPE_BUY);
   request.price        = (request.type == ORDER_TYPE_SELL
                             ? SymbolInfoDouble(symbol, SYMBOL_BID)
                             : SymbolInfoDouble(symbol, SYMBOL_ASK));
   request.deviation    = (ulong)MathMax(0, NFA_SlippagePoints);
   request.type_filling = NFA_FillingMode(symbol);
   request.comment      = "news_flatten";

   ResetLastError();
   if (!OrderSend(request, result) || !NFA_RetcodeOK(result.retcode))
   {
      // Throttled: the sweep retries every tick, so an unthrottled Print
      // would flood the journal for the whole window on a persistent error.
      if (TimeCurrent() - nfaLastCloseFail >= NFA_LOG_THROTTLE)
      {
         nfaLastCloseFail = TimeCurrent();
         Print("[NewsFilter] flatten close FAILED ticket=", ticket, " ", symbol,
               " retcode=", result.retcode, " (retrying next tick): ", event_desc);
      }
      return false;
   }

   Print("[NewsFilter] flattened ticket=", ticket, " ", symbol, ": ", event_desc);
   return true;
}

// The action layer. Call this every tick from OnTick(), passing YOUR EA's
// magic number. Only positions and orders carrying that magic are touched,
// so another EA's trades and your manual trades are always safe.
//
// Pass 0 to act on every position in the account. Do that only if this EA
// is genuinely the only thing trading it.
void NFA_Manage(const ulong magic)
{
   if (!NFA_UseNewsFilter)
      return;

   NFA_Refresh();

   // Never act on positions with a stale cache. Blocking an entry on bad
   // data costs an opportunity; closing a position on bad data costs money.
   if (nfaStale)
      return;

   if (NFA_FlattenPositions)
   {
      for (int i = PositionsTotal() - 1; i >= 0; i--)
      {
         ulong ticket = PositionGetTicket(i);
         if (ticket == 0 || !PositionSelectByTicket(ticket))
            continue;
         if (magic != 0 && (ulong)PositionGetInteger(POSITION_MAGIC) != magic)
            continue;

         string symbol = PositionGetString(POSITION_SYMBOL);
         string event_desc = "";
         if (!NFA_FlattenActive(symbol, event_desc))
            continue;

         NFA_ClosePosition(ticket, event_desc);
      }
   }

   if (NFA_CancelPendings)
   {
      for (int i = OrdersTotal() - 1; i >= 0; i--)
      {
         ulong ticket = OrderGetTicket(i);
         if (ticket == 0 || !OrderSelect(ticket))
            continue;
         if (magic != 0 && (ulong)OrderGetInteger(ORDER_MAGIC) != magic)
            continue;

         ENUM_ORDER_TYPE otype = (ENUM_ORDER_TYPE)OrderGetInteger(ORDER_TYPE);
         if (otype == ORDER_TYPE_BUY || otype == ORDER_TYPE_SELL)
            continue;   // pending orders only

         string symbol = OrderGetString(ORDER_SYMBOL);
         string event_desc = "";
         if (!NFA_CancelActive(symbol, event_desc))
            continue;

         MqlTradeRequest request;
         MqlTradeResult  result;
         ZeroMemory(request);
         ZeroMemory(result);
         request.action = TRADE_ACTION_REMOVE;
         request.order  = ticket;

         ResetLastError();
         if (!OrderSend(request, result) || !NFA_RetcodeOK(result.retcode))
            Print("[NewsFilter] failed to cancel pending ", ticket, " ", symbol,
                  " retcode=", result.retcode, " (retrying next tick): ", event_desc);
         else
            Print("[NewsFilter] cancelled pending ", ticket, " ", symbol, ": ", event_desc);
      }
   }
}

// Read-only helpers, handy for your own status printouts.
bool NFA_IsStale()    { return nfaStale; }
int  NFA_EventCount() { return ArraySize(nfaEvents); }

#endif
