//+------------------------------------------------------------------+ //| FxShow.mq5 | //| Account display reporter for www.fxhero.net. | //| | //| This EA only uploads account data for public display. It does | //| not receive commands, place orders, close positions, or modify | //| trading state. | //+------------------------------------------------------------------+ #property strict #property copyright "FxHero" #property version "1.0.0" #property description "Uploads MT5 account, position, and history data to the FXHero display website." #define FXHERO_EA_NAME "FxShow" #define FXHERO_EA_VERSION "1.0.0" #define FXHERO_DEFAULT_API_URL "https://fxhero.net/api/terminal/sync" input string InpApiUrl = FXHERO_DEFAULT_API_URL; input string InpApiKey = ""; input string InpApiSecret = ""; input int InpSyncIntervalSeconds = 10; input int InpHistoryLookbackDays = 30; input int InpMaxHistoryDeals = 100; input int InpRequestTimeoutMs = 8000; input bool InpSendOnTradeEvent = true; input bool InpShowChartComment = true; input bool InpVerboseLog = false; datetime g_last_sync_at = 0; datetime g_next_sync_at = 0; bool g_sync_requested = true; int g_last_http_code = 0; int g_last_terminal_error = 0; string g_last_message = "Not synced yet"; //+------------------------------------------------------------------+ //| Expert initialization | //+------------------------------------------------------------------+ int OnInit() { int interval = MathMax(1, InpSyncIntervalSeconds); MathSrand((uint)(GetTickCount() + TimeLocal())); EventSetTimer(interval); if(StringLen(InpApiKey) == 0 || StringLen(InpApiSecret) == 0) { g_last_message = "Missing API Key or API Secret"; Print(FXHERO_EA_NAME, ": ", g_last_message); UpdateChartComment(); return INIT_SUCCEEDED; } g_sync_requested = true; SyncNow("init"); return INIT_SUCCEEDED; } //+------------------------------------------------------------------+ //| Expert deinitialization | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); Comment(""); } //+------------------------------------------------------------------+ //| Timer | //+------------------------------------------------------------------+ void OnTimer() { datetime now = TimeCurrent(); if(g_sync_requested || now >= g_next_sync_at) SyncNow(g_sync_requested ? "event" : "timer"); UpdateChartComment(); } //+------------------------------------------------------------------+ //| Trade event | //+------------------------------------------------------------------+ void OnTradeTransaction(const MqlTradeTransaction &trans, const MqlTradeRequest &request, const MqlTradeResult &result) { if(InpSendOnTradeEvent) g_sync_requested = true; } //+------------------------------------------------------------------+ //| Main sync | //+------------------------------------------------------------------+ bool SyncNow(const string reason) { if(StringLen(InpApiKey) == 0 || StringLen(InpApiSecret) == 0) { g_last_message = "Missing API Key or API Secret"; UpdateChartComment(); return false; } string body = BuildPayload(reason); string timestamp = IntegerToString((long)TimeGMT()); string nonce = MakeNonce(); string string_to_sign = timestamp + "\n" + nonce + "\n" + body; string signature = ""; if(!HmacSha256Hex(InpApiSecret, string_to_sign, signature)) { g_last_message = "Failed to create HMAC signature"; Print(FXHERO_EA_NAME, ": ", g_last_message); UpdateChartComment(); return false; } string headers = "Content-Type: application/json; charset=utf-8\r\n" + "Accept: application/json\r\n" + "X-FxHero-Key: " + InpApiKey + "\r\n" + "X-FxHero-Timestamp: " + timestamp + "\r\n" + "X-FxHero-Nonce: " + nonce + "\r\n" + "X-FxHero-Signature: " + signature + "\r\n"; char post_data[]; StringToUtf8CharArray(body, post_data); char result[]; string result_headers = ""; ResetLastError(); int http_code = WebRequest("POST", InpApiUrl, headers, InpRequestTimeoutMs, post_data, result, result_headers); int terminal_error = GetLastError(); g_last_http_code = http_code; g_last_terminal_error = terminal_error; g_next_sync_at = TimeCurrent() + MathMax(1, InpSyncIntervalSeconds); g_sync_requested = false; string response = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); if(http_code >= 200 && http_code < 300) { g_last_sync_at = TimeCurrent(); g_last_message = "Sync OK, HTTP " + IntegerToString(http_code); if(InpVerboseLog) Print(FXHERO_EA_NAME, ": ", g_last_message, ", response=", response); UpdateChartComment(); return true; } if(http_code == -1 && terminal_error == 4014) { g_last_message = "WebRequest blocked. Add https://fxhero.net to MT5 allowed URLs."; } else { g_last_message = "Sync failed, HTTP " + IntegerToString(http_code) + ", error " + IntegerToString(terminal_error); } Print(FXHERO_EA_NAME, ": ", g_last_message, ", response=", response); UpdateChartComment(); return false; } //+------------------------------------------------------------------+ //| Payload | //+------------------------------------------------------------------+ string BuildPayload(const string reason) { string json = "{"; json += "\"account\":" + BuildAccountJson() + ","; json += "\"positions\":" + BuildPositionsJson() + ","; json += "\"history\":" + BuildHistoryJson() + ","; json += "\"meta\":{"; json += "\"ea_name\":" + JsonString(FXHERO_EA_NAME) + ","; json += "\"ea_version\":" + JsonString(FXHERO_EA_VERSION) + ","; json += "\"reason\":" + JsonString(reason) + ","; json += "\"terminal_build\":" + IntegerToString((int)TerminalInfoInteger(TERMINAL_BUILD)) + ","; json += "\"sync_interval_seconds\":" + IntegerToString(MathMax(1, InpSyncIntervalSeconds)); json += "}"; json += "}"; return json; } string BuildAccountJson() { string json = "{"; json += "\"platform\":\"mt5\","; json += "\"account_number\":" + JsonString(IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN))) + ","; json += "\"account_name\":" + JsonString(AccountInfoString(ACCOUNT_NAME)) + ","; json += "\"broker\":" + JsonString(AccountInfoString(ACCOUNT_COMPANY)) + ","; json += "\"server\":" + JsonString(AccountInfoString(ACCOUNT_SERVER)) + ","; json += "\"currency\":" + JsonString(AccountInfoString(ACCOUNT_CURRENCY)) + ","; json += "\"leverage\":" + IntegerToString((long)AccountInfoInteger(ACCOUNT_LEVERAGE)) + ","; json += "\"balance\":" + JsonNumber(AccountInfoDouble(ACCOUNT_BALANCE), 2) + ","; json += "\"equity\":" + JsonNumber(AccountInfoDouble(ACCOUNT_EQUITY), 2) + ","; json += "\"credit\":" + JsonNumber(AccountInfoDouble(ACCOUNT_CREDIT), 2) + ","; json += "\"margin\":" + JsonNumber(AccountInfoDouble(ACCOUNT_MARGIN), 2) + ","; json += "\"free_margin\":" + JsonNumber(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2) + ","; json += "\"margin_level\":" + JsonNumber(AccountInfoDouble(ACCOUNT_MARGIN_LEVEL), 2) + ","; json += "\"profit\":" + JsonNumber(AccountInfoDouble(ACCOUNT_PROFIT), 2) + ","; json += "\"server_time\":" + IntegerToString((long)TimeCurrent()); json += "}"; return json; } string BuildPositionsJson() { string json = "["; int added = 0; int total = PositionsTotal(); for(int i = 0; i < total; i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; long type = PositionGetInteger(POSITION_TYPE); string row = "{"; row += "\"ticket\":" + JsonString(IntegerToString((long)ticket)) + ","; row += "\"symbol\":" + JsonString(PositionGetString(POSITION_SYMBOL)) + ","; row += "\"type\":" + JsonString(PositionTypeText(type)) + ","; row += "\"lots\":" + JsonNumber(PositionGetDouble(POSITION_VOLUME), 4) + ","; row += "\"open_price\":" + JsonNumber(PositionGetDouble(POSITION_PRICE_OPEN), 6) + ","; row += "\"current_price\":" + JsonNumber(PositionGetDouble(POSITION_PRICE_CURRENT), 6) + ","; row += "\"sl\":" + JsonNumber(PositionGetDouble(POSITION_SL), 6) + ","; row += "\"tp\":" + JsonNumber(PositionGetDouble(POSITION_TP), 6) + ","; row += "\"profit\":" + JsonNumber(PositionGetDouble(POSITION_PROFIT), 2) + ","; row += "\"swap\":" + JsonNumber(PositionGetDouble(POSITION_SWAP), 2) + ","; row += "\"commission\":0,"; row += "\"magic_number\":" + JsonString(IntegerToString((long)PositionGetInteger(POSITION_MAGIC))) + ","; row += "\"comment\":" + JsonString(PositionGetString(POSITION_COMMENT)) + ","; row += "\"open_time\":" + IntegerToString((long)PositionGetInteger(POSITION_TIME)); row += "}"; if(added > 0) json += ","; json += row; added++; } json += "]"; return json; } string BuildHistoryJson() { int lookback_days = MathMax(1, InpHistoryLookbackDays); int max_deals = MathMax(0, InpMaxHistoryDeals); datetime to_time = TimeCurrent(); datetime from_time = to_time - (lookback_days * 86400); if(max_deals == 0 || !HistorySelect(from_time, to_time)) return "[]"; string json = "["; int added = 0; int total = HistoryDealsTotal(); for(int i = total - 1; i >= 0 && added < max_deals; i--) { ulong deal = HistoryDealGetTicket(i); if(deal == 0) continue; long deal_type = HistoryDealGetInteger(deal, DEAL_TYPE); long entry = HistoryDealGetInteger(deal, DEAL_ENTRY); if(deal_type != DEAL_TYPE_BUY && deal_type != DEAL_TYPE_SELL) continue; if(entry != DEAL_ENTRY_OUT && entry != DEAL_ENTRY_OUT_BY && entry != DEAL_ENTRY_INOUT) continue; string row = "{"; row += "\"deal_id\":" + JsonString(IntegerToString((long)deal)) + ","; row += "\"ticket\":" + JsonString(IntegerToString((long)deal)) + ","; row += "\"order_id\":" + JsonString(IntegerToString((long)HistoryDealGetInteger(deal, DEAL_ORDER))) + ","; row += "\"symbol\":" + JsonString(HistoryDealGetString(deal, DEAL_SYMBOL)) + ","; row += "\"type\":" + JsonString(DealTypeText(deal_type)) + ","; row += "\"lots\":" + JsonNumber(HistoryDealGetDouble(deal, DEAL_VOLUME), 4) + ","; row += "\"close_price\":" + JsonNumber(HistoryDealGetDouble(deal, DEAL_PRICE), 6) + ","; row += "\"profit\":" + JsonNumber(HistoryDealGetDouble(deal, DEAL_PROFIT), 2) + ","; row += "\"swap\":" + JsonNumber(HistoryDealGetDouble(deal, DEAL_SWAP), 2) + ","; row += "\"commission\":" + JsonNumber(HistoryDealGetDouble(deal, DEAL_COMMISSION), 2) + ","; row += "\"magic_number\":" + JsonString(IntegerToString((long)HistoryDealGetInteger(deal, DEAL_MAGIC))) + ","; row += "\"comment\":" + JsonString(HistoryDealGetString(deal, DEAL_COMMENT)) + ","; row += "\"close_time\":" + IntegerToString((long)HistoryDealGetInteger(deal, DEAL_TIME)); row += "}"; if(added > 0) json += ","; json += row; added++; } json += "]"; return json; } //+------------------------------------------------------------------+ //| HMAC-SHA256 | //+------------------------------------------------------------------+ bool HmacSha256Hex(const string secret, const string message, string &hex) { uchar key[]; uchar msg[]; StringToUtf8Bytes(secret, key); StringToUtf8Bytes(message, msg); if(ArraySize(key) > 64) { uchar hashed_key[]; if(!Sha256(key, hashed_key)) return false; ArrayCopyBytes(hashed_key, key); } uchar key_block[]; ArrayResize(key_block, 64); for(int i = 0; i < 64; i++) key_block[i] = 0; for(int i = 0; i < ArraySize(key) && i < 64; i++) key_block[i] = key[i]; uchar inner[]; uchar outer[]; ArrayResize(inner, 64 + ArraySize(msg)); ArrayResize(outer, 64 + 32); for(int i = 0; i < 64; i++) { inner[i] = (uchar)(key_block[i] ^ 0x36); outer[i] = (uchar)(key_block[i] ^ 0x5c); } for(int i = 0; i < ArraySize(msg); i++) inner[64 + i] = msg[i]; uchar inner_hash[]; if(!Sha256(inner, inner_hash)) return false; for(int i = 0; i < 32; i++) outer[64 + i] = inner_hash[i]; uchar digest[]; if(!Sha256(outer, digest)) return false; hex = BytesToHex(digest); return true; } bool Sha256(uchar &data[], uchar &digest[]) { uchar key[]; ArrayResize(key, 0); ArrayResize(digest, 0); int result = CryptEncode(CRYPT_HASH_SHA256, data, key, digest); return (result > 0 && ArraySize(digest) == 32); } //+------------------------------------------------------------------+ //| Helpers | //+------------------------------------------------------------------+ void StringToUtf8Bytes(const string value, uchar &bytes[]) { ArrayResize(bytes, 0); int copied = StringToCharArray(value, bytes, 0, WHOLE_ARRAY, CP_UTF8); if(copied <= 0) return; int size = ArraySize(bytes); if(size > 0 && bytes[size - 1] == 0) ArrayResize(bytes, size - 1); } void StringToUtf8CharArray(const string value, char &bytes[]) { uchar raw[]; StringToUtf8Bytes(value, raw); ArrayResize(bytes, ArraySize(raw)); for(int i = 0; i < ArraySize(raw); i++) bytes[i] = (char)raw[i]; } void ArrayCopyBytes(uchar &source[], uchar &target[]) { ArrayResize(target, ArraySize(source)); for(int i = 0; i < ArraySize(source); i++) target[i] = source[i]; } string BytesToHex(uchar &bytes[]) { string hex = ""; string chars = "0123456789abcdef"; for(int i = 0; i < ArraySize(bytes); i++) { int value = (int)bytes[i]; hex += StringSubstr(chars, value / 16, 1); hex += StringSubstr(chars, value % 16, 1); } return hex; } string MakeNonce() { string seed = IntegerToString((long)TimeLocal()) + "-" + IntegerToString((long)GetTickCount()) + "-" + IntegerToString(MathRand()) + "-" + IntegerToString(MathRand()) + "-" + IntegerToString((long)AccountInfoInteger(ACCOUNT_LOGIN)); uchar bytes[]; uchar digest[]; StringToUtf8Bytes(seed, bytes); if(!Sha256(bytes, digest)) return IntegerToString((long)TimeLocal()) + IntegerToString(MathRand()); return StringSubstr(BytesToHex(digest), 0, 32); } string JsonString(const string value) { return "\"" + JsonEscape(value) + "\""; } string JsonEscape(const string value) { string out = ""; int length = StringLen(value); for(int i = 0; i < length; i++) { ushort ch = StringGetCharacter(value, i); if(ch == 34) out += "\\\""; else if(ch == 92) out += "\\\\"; else if(ch == 8) out += "\\b"; else if(ch == 12) out += "\\f"; else if(ch == 10) out += "\\n"; else if(ch == 13) out += "\\r"; else if(ch == 9) out += "\\t"; else if(ch < 32) out += StringFormat("\\u%04X", ch); else out += ShortToString(ch); } return out; } string JsonNumber(const double value, const int digits) { if(!MathIsValidNumber(value)) return "0"; return DoubleToString(value, digits); } string PositionTypeText(const long type) { if(type == POSITION_TYPE_BUY) return "buy"; if(type == POSITION_TYPE_SELL) return "sell"; return "unknown"; } string DealTypeText(const long type) { if(type == DEAL_TYPE_BUY) return "buy"; if(type == DEAL_TYPE_SELL) return "sell"; return "unknown"; } void UpdateChartComment() { if(!InpShowChartComment) return; string text = FXHERO_EA_NAME + " " + FXHERO_EA_VERSION + "\n"; text += "API: " + InpApiUrl + "\n"; text += "Last sync: " + (g_last_sync_at > 0 ? TimeToString(g_last_sync_at, TIME_DATE | TIME_SECONDS) : "-") + "\n"; text += "HTTP: " + IntegerToString(g_last_http_code) + ", Terminal error: " + IntegerToString(g_last_terminal_error) + "\n"; text += "Status: " + g_last_message; Comment(text); }