forked from MrBaro75/Warrior_EA
188 lines
7.9 KiB
MQL5
188 lines
7.9 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| AltDataHttpClient.mqh |
| |||
//| AnimateDread |
| |||
//| Generic HTTP client for AltDataFetch's per-source pipelines: the |
| |||
//| per-host 4014 backoff, URL masking for the journal, and the |
| |||
//| minimal JSON/URL-encoding helpers the Socrata/FRED/EIA/CBOE |
| |||
//| responses need. Knows nothing about FRED/COT/EIA/GEX - only how |
| |||
//| to fetch a URL and parse a flat JSON body. |
| |||
//+------------------------------------------------------------------+
| |||
#ifndef ALTDATAHTTPCLIENT_MQH
| |||
#define ALTDATAHTTPCLIENT_MQH
| |||
| |||
#define ALTFETCH_TIMEOUT_MS 15000
| |||
//--- the SPX option chain is ~13 MB; the small feeds' 15 s would time out on a slow link
| |||
#define ALTFETCH_GEX_TIMEOUT_MS 90000
| |||
| |||
class CAltDataHttpClient
| |||
{
| |||
private:
| |||
string m_blockedHost[]; // 4014 backoff, PER HOST - one missing whitelist entry
| |||
datetime m_blockedUntil[]; // must not silence the hosts that are whitelisted
| |||
bool m_urlAlerted; // full four-line reference printed once per session
| |||
| |||
//--- "https://host/..." -> "https://host" (what the MT5 whitelist dialog wants)
| |||
string HostOf(string url)
| |||
{
| |||
int p = StringFind(url, "://");
| |||
int q = StringFind(url, "/", p < 0 ? 0 : p + 3);
| |||
return (q < 0) ? url : StringSubstr(url, 0, q);
| |||
}
| |||
| |||
//--- URL safe for the JOURNAL: api_key values masked. The 2026-08-16 EIA failure
| |||
//--- printed the first 80 chars of the URL - which included most of the key.
| |||
//--- Every error path that echoes a URL must go through this.
| |||
string MaskUrl(string url)
| |||
{
| |||
int p = StringFind(url, "api_key=");
| |||
if(p < 0)
| |||
return StringSubstr(url, 0, 100);
| |||
int e = StringFind(url, "&", p);
| |||
string masked = StringSubstr(url, 0, p + 8) + "***";
| |||
if(e > 0)
| |||
masked += StringSubstr(url, e);
| |||
return StringSubstr(masked, 0, 100);
| |||
}
| |||
| |||
int BlockedIndex(string host)
| |||
{
| |||
for(int i = 0; i < ArraySize(m_blockedHost); i++)
| |||
if(m_blockedHost[i] == host)
| |||
return i;
| |||
return -1;
| |||
}
| |||
| |||
public:
| |||
CAltDataHttpClient(void) : m_urlAlerted(false) {}
| |||
| |||
bool HttpGet(string url, string &body, int timeoutMs = ALTFETCH_TIMEOUT_MS)
| |||
{
| |||
//--- PER-HOST backoff. An in-flight request cannot be cancelled; refusing to START another
| |||
//--- one is the whole of the remedy.
| |||
if(IsStopped())
| |||
return false;
| |||
string host = HostOf(url);
| |||
int bi = BlockedIndex(host);
| |||
if(bi >= 0 && TimeCurrent() < m_blockedUntil[bi])
| |||
return false;
| |||
char data[], result[];
| |||
string rh;
| |||
ResetLastError();
| |||
//--- identify ourselves: some API gateways reject requests with an empty User-Agent
| |||
//--- at the transport level (candidate cause of the EIA 1003 on first live fetch)
| |||
int status = WebRequest("GET", url, "User-Agent: WarriorEA/1.0\r\n", timeoutMs, data, result, rh);
| |||
if(status == -1)
| |||
{
| |||
int err = GetLastError();
| |||
if(err == 4014)
| |||
{
| |||
//--- Backoff, not a permanent latch: the whitelist takes effect for a RUNNING EA
| |||
//--- the moment the user saves the options dialog, so retrying hourly picks the
| |||
//--- fix up without a re-attach.
| |||
bool firstTimeThisHost = (bi < 0);
| |||
if(bi < 0)
| |||
{
| |||
bi = ArraySize(m_blockedHost);
| |||
ArrayResize(m_blockedHost, bi + 1);
| |||
ArrayResize(m_blockedUntil, bi + 1);
| |||
m_blockedHost[bi] = host;
| |||
}
| |||
m_blockedUntil[bi] = TimeCurrent() + 3600;
| |||
if(firstTimeThisHost)
| |||
{
| |||
//--- ALWAYS name the offender - the whole point of this message is telling
| |||
//--- the user which entry is missing, not that "something" is
| |||
Alert("Warrior EA: web request to ", host, " is blocked (4014) - this exact ",
| |||
"address is missing from the WebRequest whitelist (or the checkbox is ",
| |||
"off). Copy-paste line and steps are in the Experts log.");
| |||
Print("AltDataFetch: ==================================================================");
| |||
Print("AltDataFetch: BLOCKED host (add this exact line as its own whitelist entry):");
| |||
Print(host);
| |||
Print("AltDataFetch: MT5 menu: Tools > Options > Expert Advisors ->");
| |||
Print("AltDataFetch: tick 'Allow WebRequest for listed URL', add the line, click OK.");
| |||
Print("AltDataFetch: Takes effect immediately - the EA retries within the hour.");
| |||
if(!m_urlAlerted)
| |||
{
| |||
m_urlAlerted = true;
| |||
Print("AltDataFetch: For reference, the FULL set this EA uses (each its own entry):");
| |||
Print("https://publicreporting.cftc.gov");
| |||
Print("https://api.stlouisfed.org");
| |||
Print("https://api.eia.gov");
| |||
Print("https://cdn.cboe.com");
| |||
Print("AltDataFetch: Until fixed it runs normally on cached alt-data files - features");
| |||
Print("AltDataFetch: stay valid but stop updating past their last downloaded date.");
| |||
}
| |||
Print("AltDataFetch: ==================================================================");
| |||
}
| |||
else
| |||
Print("AltDataFetch: still blocked (4014): " + host + " - retrying in an hour.");
| |||
}
| |||
else
| |||
PrintFormat("AltDataFetch: WebRequest error %d for %s", err, MaskUrl(url));
| |||
return false;
| |||
}
| |||
if(status != 200)
| |||
{
| |||
PrintFormat("AltDataFetch: HTTP %d for %s%s", status, MaskUrl(url),
| |||
status >= 1000 ? " (1xxx = MT5 transport-layer failure, not a server "
| |||
"response - retried on the normal hourly schedule)" : "");
| |||
return false;
| |||
}
| |||
body = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8);
| |||
return true;
| |||
}
| |||
| |||
//--- minimal targeted JSON scan: value of "key":"..." (or numeric) at/after position `from`.
| |||
//--- Sufficient for Socrata/FRED responses, which are flat arrays of flat objects.
| |||
string JsonField(const string &body, string key, int from, int &valueEnd)
| |||
{
| |||
valueEnd = -1;
| |||
string tag = "\"" + key + "\":";
| |||
int p = StringFind(body, tag, from);
| |||
if(p < 0)
| |||
return "";
| |||
p += StringLen(tag);
| |||
if(StringGetCharacter(body, p) == '"')
| |||
{
| |||
int q = StringFind(body, "\"", p + 1);
| |||
if(q < 0)
| |||
return "";
| |||
valueEnd = q + 1;
| |||
return StringSubstr(body, p + 1, q - p - 1);
| |||
}
| |||
int e = p;
| |||
while(e < StringLen(body))
| |||
{
| |||
ushort c = StringGetCharacter(body, e);
| |||
if((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E')
| |||
e++;
| |||
else
| |||
break;
| |||
}
| |||
valueEnd = e;
| |||
return StringSubstr(body, p, e - p);
| |||
}
| |||
| |||
string UrlEncodePart(string s)
| |||
{
| |||
string out = "";
| |||
for(int i = 0; i < StringLen(s); i++)
| |||
{
| |||
ushort c = StringGetCharacter(s, i);
| |||
//--- '%' MUST come first and MUST be escaped: SoQL like-predicates use it as the
| |||
//--- wildcard ('EURO FX -%'), and an unescaped one turns the rest of the query into
| |||
//--- a malformed percent-escape.
| |||
if(c == '%') out += "%25";
| |||
else if(c == ' ') out += "%20";
| |||
else if(c == '\'') out += "%27";
| |||
else if(c == '&') out += "%26";
| |||
else if(c == '>') out += "%3E";
| |||
else if(c == '=') out += "%3D";
| |||
else if(c == '+') out += "%2B";
| |||
else if(c == '#') out += "%23";
| |||
else out += ShortToString(c);
| |||
}
| |||
return out;
| |||
}
| |||
};
| |||
#endif
|