447 lines
16 KiB
MQL5
447 lines
16 KiB
MQL5
#ifndef AI_GATEWAY_MQH
|
|
#define AI_GATEWAY_MQH
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// AIGateway.mqh — Unified AI Gateway for profitgtx
|
|
// Routes EA requests through n8n → OpenRouter → Claude Sonnet
|
|
// Actions: validate_trade, get_briefing, get_session, get_news_bias,
|
|
// report_trade, watchlist_status, ping
|
|
//
|
|
// URL routing is handled by AutoDetectDeployment() which sets g_gw_url.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
string GW_LastError = "";
|
|
|
|
// ─── JSON helpers ────────────────────────────────────────────────────────────
|
|
|
|
string GWJsonEscape(const string s)
|
|
{
|
|
string x = s;
|
|
StringReplace(x, "\\", "\\\\");
|
|
StringReplace(x, "\"", "\\\"");
|
|
StringReplace(x, "\n", " ");
|
|
StringReplace(x, "\r", " ");
|
|
return x;
|
|
}
|
|
|
|
bool GWExtractString(const string &json, const string &key, string &out)
|
|
{
|
|
int k = StringFind(json, "\"" + key + "\"");
|
|
if(k < 0) return false;
|
|
int colon = StringFind(json, ":", k);
|
|
if(colon < 0) return false;
|
|
int q1 = StringFind(json, "\"", colon + 1);
|
|
if(q1 < 0) return false;
|
|
int q2 = StringFind(json, "\"", q1 + 1);
|
|
if(q2 < 0) return false;
|
|
out = StringSubstr(json, q1 + 1, q2 - q1 - 1);
|
|
return true;
|
|
}
|
|
|
|
bool GWExtractNumber(const string &json, const string &key, double &out)
|
|
{
|
|
int k = StringFind(json, "\"" + key + "\"");
|
|
if(k < 0) return false;
|
|
int colon = StringFind(json, ":", k);
|
|
if(colon < 0) return false;
|
|
int endComma = StringFind(json, ",", colon);
|
|
int endBrace = StringFind(json, "}", colon);
|
|
int end = endComma;
|
|
if(end < 0 || (endBrace >= 0 && endBrace < end)) end = endBrace;
|
|
if(end < 0) return false;
|
|
string val = StringSubstr(json, colon + 1, end - (colon + 1));
|
|
StringTrimLeft(val);
|
|
StringTrimRight(val);
|
|
out = StringToDouble(val);
|
|
return true;
|
|
}
|
|
|
|
bool GWExtractBool(const string &json, const string &key, bool &out)
|
|
{
|
|
int k = StringFind(json, "\"" + key + "\"");
|
|
if(k < 0) return false;
|
|
int colon = StringFind(json, ":", k);
|
|
if(colon < 0) return false;
|
|
string rest = StringSubstr(json, colon + 1, 10);
|
|
StringTrimLeft(rest);
|
|
StringToLower(rest);
|
|
out = (StringFind(rest, "true") == 0);
|
|
return true;
|
|
}
|
|
|
|
// ─── Deployment auto-detection ────────────────────────────────────────────────
|
|
//
|
|
// Probes the internal NN endpoint with a 1.5 s timeout.
|
|
// If it responds (any HTTP code) → container socat bridges are up → use internal URLs.
|
|
// Otherwise → laptop / VPN → use public HTTPS URLs.
|
|
// DeploymentProfile input overrides AUTO behaviour.
|
|
|
|
void AutoDetectDeployment()
|
|
{
|
|
if(DeploymentProfile == "CONTAINER")
|
|
{
|
|
g_nn_url = NN_URL_Internal;
|
|
g_gw_url = GW_URL_Internal;
|
|
g_is_container = true;
|
|
Print("Deploy: CONTAINER (manual) | NN=", g_nn_url, " | GW=", g_gw_url);
|
|
return;
|
|
}
|
|
|
|
if(DeploymentProfile == "LAPTOP")
|
|
{
|
|
g_nn_url = NN_URL_Laptop;
|
|
g_gw_url = GW_URL_Laptop;
|
|
g_is_container = false;
|
|
Print("Deploy: LAPTOP (manual) | NN=", g_nn_url, " | GW=", g_gw_url);
|
|
return;
|
|
}
|
|
|
|
// AUTO — probe internal NN endpoint
|
|
string probe_url = NN_URL_Internal;
|
|
char probe_data[], probe_result[];
|
|
string probe_headers = "Content-Type: application/json\r\n";
|
|
string probe_body = "{\"symbol\":\"probe\",\"tf\":\"M1\",\"features\":{},\"feature_vector\":[]}";
|
|
StringToCharArray(probe_body, probe_data, 0, WHOLE_ARRAY, CP_UTF8);
|
|
string probe_resp_headers;
|
|
|
|
ResetLastError();
|
|
int code = WebRequest("POST", probe_url, probe_headers, 1500,
|
|
probe_data, probe_result, probe_resp_headers);
|
|
|
|
if(code > 0) // any server response means socat bridge is alive
|
|
{
|
|
g_nn_url = NN_URL_Internal;
|
|
g_gw_url = GW_URL_Internal;
|
|
g_is_container = true;
|
|
Print("Deploy: AUTO → CONTAINER (internal NN responded HTTP ", code, ")");
|
|
}
|
|
else
|
|
{
|
|
g_nn_url = NN_URL_Laptop;
|
|
g_gw_url = GW_URL_Laptop;
|
|
g_is_container = false;
|
|
Print("Deploy: AUTO → LAPTOP (internal NN unreachable, err=", GetLastError(), ") — using HTTPS");
|
|
}
|
|
|
|
Print(" NN URL: ", g_nn_url);
|
|
Print(" GW URL: ", g_gw_url);
|
|
}
|
|
|
|
// ─── Core HTTP POST ───────────────────────────────────────────────────────────
|
|
|
|
bool GatewayRequest(const string action, const string json_payload, string &response)
|
|
{
|
|
if(AI_Mode == "off")
|
|
return false;
|
|
|
|
if(StringLen(g_gw_url) < 10)
|
|
{
|
|
GW_LastError = "Gateway URL not resolved (AutoDetectDeployment not called?)";
|
|
return false;
|
|
}
|
|
|
|
string headers = "Content-Type: application/json\r\n";
|
|
if(StringLen(AI_Gateway_Token) > 0)
|
|
headers += "X-Gateway-Token: " + AI_Gateway_Token + "\r\n";
|
|
|
|
char post_data[], result_data[];
|
|
string result_headers;
|
|
StringToCharArray(json_payload, post_data, 0, StringLen(json_payload), CP_UTF8);
|
|
|
|
ResetLastError();
|
|
int code = WebRequest("POST", g_gw_url, headers, AI_TimeoutMs, post_data, result_data, result_headers);
|
|
|
|
if(code <= 0 || code >= 400)
|
|
{
|
|
GW_LastError = "HTTP " + IntegerToString(code) + " err=" + IntegerToString(GetLastError());
|
|
GW_Status = "ERROR";
|
|
return false;
|
|
}
|
|
|
|
response = CharArrayToString(result_data, 0, -1, CP_UTF8);
|
|
GW_Status = "ACTIVE";
|
|
return true;
|
|
}
|
|
|
|
// ─── Initialize ───────────────────────────────────────────────────────────────
|
|
|
|
bool InitializeAIGateway()
|
|
{
|
|
GW_Initialized = false;
|
|
GW_Status = "NOT_INIT";
|
|
GW_LastError = "";
|
|
|
|
if(AI_Mode == "off")
|
|
{
|
|
GW_Status = "DISABLED";
|
|
Print("AIGateway: Mode=off — AI calls disabled");
|
|
return true;
|
|
}
|
|
|
|
string ping = "{\"action\":\"ping\",\"symbol\":\"" + GWJsonEscape(_Symbol) + "\"}";
|
|
string resp;
|
|
|
|
if(!GatewayRequest("ping", ping, resp))
|
|
{
|
|
if(AI_Mode == "advisory")
|
|
{
|
|
Print("AIGateway: Gateway unreachable — advisory mode, EA runs without AI gate | ", GW_LastError);
|
|
GW_Status = "OFFLINE_ADVISORY";
|
|
return true;
|
|
}
|
|
Print("AIGateway: Gateway init FAILED (mandatory mode) — ", GW_LastError);
|
|
return false;
|
|
}
|
|
|
|
GW_Initialized = true;
|
|
GW_Status = "ACTIVE";
|
|
Print("AIGateway: OK | URL=", g_gw_url, " | Mode=", AI_Mode,
|
|
" | env=", (g_is_container ? "CONTAINER" : "LAPTOP"));
|
|
return true;
|
|
}
|
|
|
|
bool IsAIEnabled() { return GW_Initialized; }
|
|
|
|
// ─── Trade Validation ────────────────────────────────────────────────────────
|
|
|
|
bool AIValidateTrade(const string setup_name, const bool is_buy, double &size_multiplier)
|
|
{
|
|
if(AI_Mode == "off")
|
|
return true;
|
|
|
|
if(!GW_Initialized)
|
|
{
|
|
if(AI_Mode == "advisory") { size_multiplier *= 0.5; return true; }
|
|
return false;
|
|
}
|
|
|
|
if(!Use_Watchlist_Gate || IsSymbolActiveTrading(_Symbol))
|
|
{
|
|
// Watchlist check passed — continue to trade validation
|
|
}
|
|
else
|
|
{
|
|
Last_AI_Analysis = "[WL] " + _Symbol + " not in active-trading top-10";
|
|
return false;
|
|
}
|
|
|
|
string dir = is_buy ? "BUY" : "SELL";
|
|
string payload = "{";
|
|
payload += "\"action\":\"validate_trade\",";
|
|
payload += "\"symbol\":\"" + GWJsonEscape(_Symbol) + "\",";
|
|
payload += "\"timeframe\":\"" + EnumToString((ENUM_TIMEFRAMES)Period()) + "\",";
|
|
payload += "\"direction\":\"" + dir + "\",";
|
|
payload += "\"setup\":\"" + GWJsonEscape(setup_name) + "\",";
|
|
payload += "\"nn_bias\":" + IntegerToString(NN_Bias) + ",";
|
|
payload += "\"nn_confidence\":" + DoubleToString(NN_Confidence, 2) + ",";
|
|
payload += "\"mode\":\"" + GWJsonEscape(ModeToString(Current_Mode)) + "\",";
|
|
payload += "\"state\":\"" + GWJsonEscape(StateToString(Current_State)) + "\",";
|
|
payload += "\"warnings\":" + IntegerToString(Active_Warnings) + ",";
|
|
payload += "\"praise\":" + IntegerToString(Active_Praise_Signals) + ",";
|
|
payload += "\"cluster_strength\":" + DoubleToString(Coordinator_Cluster_Strength, 2) + ",";
|
|
payload += "\"conflict_score\":" + DoubleToString(Coordinator_Conflict_Score, 2) + ",";
|
|
payload += "\"news_bias\":" + IntegerToString(News_Bias_Direction) + ",";
|
|
payload += "\"news_strength\":" + DoubleToString(News_Bias_Strength, 2) + ",";
|
|
payload += "\"news_block\":" + (News_Trade_Block_Active || GW_News_Block_Flag ? "true" : "false") + ",";
|
|
payload += "\"news_allowed_direction\":" + IntegerToString(News_Trade_Allowed_Direction) + ",";
|
|
double point = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
double sl_pts = (Manual_SL_Points > 0) ? Manual_SL_Points * point : 0;
|
|
double tp_pts = (Manual_TP_Points > 0) ? Manual_TP_Points * point : 0;
|
|
double est_sl = is_buy ? bid - sl_pts : bid + sl_pts;
|
|
double est_tp = is_buy ? bid + tp_pts : bid - tp_pts;
|
|
payload += "\"entry\":" + DoubleToString(bid, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)) + ",";
|
|
payload += "\"sl\":" + DoubleToString(est_sl, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)) + ",";
|
|
payload += "\"tp\":" + DoubleToString(est_tp, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
|
|
payload += "}";
|
|
|
|
string resp;
|
|
if(!GatewayRequest("validate_trade", payload, resp))
|
|
{
|
|
if(AI_Mode == "advisory") { size_multiplier *= 0.5; return true; }
|
|
return false;
|
|
}
|
|
|
|
string decision = "", reason = "";
|
|
double confidence = 70.0, new_mult = 1.0;
|
|
GWExtractString(resp, "decision", decision);
|
|
GWExtractString(resp, "reason", reason);
|
|
GWExtractNumber(resp, "confidence", confidence);
|
|
GWExtractNumber(resp, "size_multiplier", new_mult);
|
|
|
|
Last_AI_Analysis = "[GW:" + dir + "] " + decision + " | " + reason;
|
|
|
|
string u = decision;
|
|
StringToUpper(u);
|
|
|
|
if(StringFind(u, "YES") >= 0)
|
|
{
|
|
size_multiplier *= MathMax(0.1, new_mult);
|
|
AI_Validation_Accepts++;
|
|
return true;
|
|
}
|
|
if(StringFind(u, "MAYBE") >= 0)
|
|
{
|
|
size_multiplier *= MathMin(0.5, MathMax(0.1, new_mult));
|
|
AI_Validation_Accepts++;
|
|
return true;
|
|
}
|
|
if(StringFind(u, "NO") >= 0)
|
|
{
|
|
AI_Validation_Rejects++;
|
|
return false;
|
|
}
|
|
|
|
// Unknown response — fall back to advisory behaviour
|
|
if(AI_Mode == "advisory") { size_multiplier *= 0.5; return true; }
|
|
AI_Validation_Rejects++;
|
|
return false;
|
|
}
|
|
|
|
// ─── Multi-TF Briefing ───────────────────────────────────────────────────────
|
|
|
|
string AIFetchBriefing(const string tf_str)
|
|
{
|
|
if(AI_Mode == "off" || !GW_Initialized)
|
|
return "";
|
|
|
|
string payload = "{";
|
|
payload += "\"action\":\"get_briefing\",";
|
|
payload += "\"symbol\":\"" + GWJsonEscape(_Symbol) + "\",";
|
|
payload += "\"timeframe\":\"" + tf_str + "\",";
|
|
payload += "\"price\":" + DoubleToString(iClose(_Symbol, PERIOD_CURRENT, 0), 5) + ",";
|
|
payload += "\"session\":\"" + GWJsonEscape(GW_Session_Name) + "\"";
|
|
payload += "}";
|
|
|
|
string resp;
|
|
if(!GatewayRequest("get_briefing", payload, resp))
|
|
return "";
|
|
|
|
string bias = "", regime = "", plan = "";
|
|
double conf = 0.0;
|
|
GWExtractString(resp, "bias", bias);
|
|
GWExtractString(resp, "regime", regime);
|
|
GWExtractString(resp, "plan", plan);
|
|
GWExtractNumber(resp, "confidence", conf);
|
|
|
|
return tf_str + ": " + bias + " (" + regime + ") " + DoubleToString(conf, 0) + "% — " + plan;
|
|
}
|
|
|
|
// ─── Session Briefing ────────────────────────────────────────────────────────
|
|
|
|
void AIFetchSessionBriefing()
|
|
{
|
|
if(AI_Mode == "off" || !GW_Initialized)
|
|
return;
|
|
|
|
datetime now = TimeCurrent();
|
|
MqlDateTime dt;
|
|
TimeToStruct(now, dt);
|
|
string server_time = StringFormat("%04d-%02d-%02d %02d:%02d",
|
|
dt.year, dt.mon, dt.day, dt.hour, dt.min);
|
|
|
|
string payload = "{";
|
|
payload += "\"action\":\"get_session\",";
|
|
payload += "\"symbol\":\"" + GWJsonEscape(_Symbol) + "\",";
|
|
payload += "\"server_time\":\"" + server_time + "\",";
|
|
payload += "\"price\":" + DoubleToString(iClose(_Symbol, PERIOD_CURRENT, 0), 5) + ",";
|
|
payload += "\"h4_bias\":\"" + GWJsonEscape(GW_Brief_H4) + "\"";
|
|
payload += "}";
|
|
|
|
string resp;
|
|
if(!GatewayRequest("get_session", payload, resp))
|
|
return;
|
|
|
|
GWExtractString(resp, "phase", GW_Session_Phase);
|
|
GWExtractString(resp, "session", GW_Session_Name);
|
|
GWExtractNumber(resp, "confidence", GW_Session_Confidence);
|
|
}
|
|
|
|
// ─── Live News Bias ──────────────────────────────────────────────────────────
|
|
//
|
|
// Enriched payload: sends actual MT5 calendar event details (name, currency,
|
|
// actual vs forecast, importance, minutes_until) so Claude can reason about
|
|
// the specific event rather than just the symbol name.
|
|
|
|
void AIFetchNewsBias()
|
|
{
|
|
if(AI_Mode == "off" || !GW_Initialized)
|
|
return;
|
|
|
|
string payload = "{";
|
|
payload += "\"action\":\"get_news_bias\",";
|
|
payload += "\"symbol\":\"" + GWJsonEscape(_Symbol) + "\",";
|
|
payload += "\"timeframe\":\"" + EnumToString((ENUM_TIMEFRAMES)Period()) + "\",";
|
|
payload += "\"base_currency\":\"" + g_symbol_base + "\",";
|
|
payload += "\"quote_currency\":\"" + g_symbol_quote + "\",";
|
|
payload += "\"local_bias\":" + IntegerToString(News_Bias_Direction) + ",";
|
|
payload += "\"local_strength\":" + DoubleToString(News_Bias_Strength, 2) + ",";
|
|
payload += "\"pending_events\":" + News_Pending_Events_JSON;
|
|
payload += "}";
|
|
|
|
string resp;
|
|
if(!GatewayRequest("get_news_bias", payload, resp))
|
|
return;
|
|
|
|
double bias_dir = 0.0, bias_str = 0.0;
|
|
bool block_flag = false;
|
|
string headlines = "";
|
|
|
|
GWExtractNumber(resp, "bias_direction", bias_dir);
|
|
GWExtractNumber(resp, "bias_strength", bias_str);
|
|
GWExtractBool(resp, "news_block_flag", block_flag);
|
|
GWExtractString(resp, "headlines", headlines);
|
|
|
|
// Override local news bias only if gateway provides a stronger signal
|
|
if(MathAbs(bias_dir) > 0.0 && bias_str > News_Bias_Strength)
|
|
{
|
|
News_Bias_Direction = (int)MathRound(bias_dir);
|
|
News_Bias_Strength = bias_str;
|
|
}
|
|
|
|
if(block_flag)
|
|
GW_News_Block_Flag = true;
|
|
|
|
if(StringLen(headlines) > 0)
|
|
GW_News_Headlines = headlines;
|
|
}
|
|
|
|
// ─── Trade Reporting (fire-and-forget after position close) ─────────────────
|
|
|
|
void AIReportTrade(const double profit, const string setup, const string direction, const int duration_min)
|
|
{
|
|
if(AI_Mode == "off")
|
|
return;
|
|
|
|
string payload = "{";
|
|
payload += "\"action\":\"report_trade\",";
|
|
payload += "\"symbol\":\"" + GWJsonEscape(_Symbol) + "\",";
|
|
payload += "\"profit\":" + DoubleToString(profit, 2) + ",";
|
|
payload += "\"setup\":\"" + GWJsonEscape(setup) + "\",";
|
|
payload += "\"direction\":\"" + direction + "\",";
|
|
payload += "\"duration_min\":" + IntegerToString(duration_min);
|
|
payload += "}";
|
|
|
|
string resp;
|
|
GatewayRequest("report_trade", payload, resp); // ignore result
|
|
}
|
|
|
|
// ─── Watchlist Status ────────────────────────────────────────────────────────
|
|
|
|
bool IsSymbolActiveTrading(const string symbol)
|
|
{
|
|
if(!Use_Watchlist_Gate || !GW_Initialized)
|
|
return true;
|
|
|
|
string payload = "{\"action\":\"watchlist_status\",\"symbol\":\"" + GWJsonEscape(symbol) + "\"}";
|
|
string resp;
|
|
if(!GatewayRequest("watchlist_status", payload, resp))
|
|
return true; // fail open
|
|
|
|
bool active = true;
|
|
GWExtractBool(resp, "active", active);
|
|
GW_Symbol_Active = active;
|
|
return active;
|
|
}
|
|
|
|
#endif
|