//+------------------------------------------------------------------+ //| TheBotAnalyzerBridge.mq4 | //| Pont de télémétrie en lecture seule vers TheBotAnalyzer. | //| | //| Équivalent MetaTrader 4 de TheBotAnalyzerBridge.mq5. La logique et | //| la charge utile envoyée au serveur sont identiques ; seule | //| l'API du terminal change — MQL4 n'a pas de notion de « deal », | //| chaque ordre clôturé est une ligne d'historique à part entière, | //| et la fermeture passe par OrderClose plutôt que par CTrade. | //| | //| Ce que fait cet Expert Advisor : | //| - envoie périodiquement l'état du compte (solde, équité, | //| marge, positions ouvertes) ; | //| - remonte les transactions clôturées et les positions | //| ouvertes ; | //| - récupère les commandes en attente et les exécute. | //| | //| Ce qu'il ne fait pas, et ne peut pas faire : | //| - ouvrir une position, quelle qu'elle soit ; | //| - modifier un stop ou un objectif ; | //| - transmettre le moindre mot de passe. | //| | //| Les seules commandes interprétées sont : suspendre_ea, | //| reprendre_ea, fermer_positions, fermer_symbole, ping. | //+------------------------------------------------------------------+ #property copyright "TheBotAnalyzer" #property version "1.00" #property strict //--- Paramètres ------------------------------------------------------ input string ServeurURL = "https://exemple.tld/api/bridge/v1"; // Adresse de l'API (fiche du compte) input string JetonCompte = ""; // Jeton de 48 caractères input int IntervalleSec = 60; // Intervalle entre deux signaux input int HistoriqueJours = 30; // Profondeur d'historique au démarrage input bool AutoriserFermeture = true; // Exécuter les ordres de fermeture input int GlissementPoints = 30; // Glissement toléré à la fermeture input bool JournalDetaille = false; // Trace complète dans l'onglet Experts //--- État interne ---------------------------------------------------- datetime dernierEnvoi = 0; bool liaisonEtablie = false; bool eaSuspendu = false; long magicsSuspendus[]; //+------------------------------------------------------------------+ int OnInit() { if(StringLen(JetonCompte) < 32) { Print("[TheBotAnalyzer] Jeton absent ou trop court. Collez celui affiché sur la fiche du compte."); return(INIT_PARAMETERS_INCORRECT); } if(!IsTradeAllowed()) Print("[TheBotAnalyzer] Le trading automatique est désactivé : les commandes de fermeture ne pourront pas s'exécuter."); EventSetTimer(MathMax(10, IntervalleSec)); if(Handshake()) { liaisonEtablie = true; Print("[TheBotAnalyzer] Liaison établie. Envoi toutes les ", IntervalleSec, " secondes."); EnvoyerHistorique(HistoriqueJours); } else { Print("[TheBotAnalyzer] Liaison impossible. Vérifiez le jeton et l'autorisation WebRequest de l'adresse."); } return(INIT_SUCCEEDED); } //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); } //+------------------------------------------------------------------+ void OnTimer() { if(!liaisonEtablie) { liaisonEtablie = Handshake(); if(!liaisonEtablie) return; } EnvoyerEtatCompte(); EnvoyerHistorique(2); // fenêtre glissante courte : les clôtures récentes EnvoyerPositionsOuvertes(); TraiterCommandes(); dernierEnvoi = TimeCurrent(); } //+------------------------------------------------------------------+ //| L'EA ne trade pas : OnTick sert uniquement de secours si le timer | //| ne se déclenche pas (terminal en arrière-plan sur certains VPS). | //+------------------------------------------------------------------+ void OnTick() { if(TimeCurrent() - dernierEnvoi >= IntervalleSec * 2) OnTimer(); } //+------------------------------------------------------------------+ //| Vérification de liaison | //+------------------------------------------------------------------+ bool Handshake() { string reponse; int code = AppelHttp("GET", "/handshake", "", reponse); if(code != 200) { if(JournalDetaille) Print("[TheBotAnalyzer] Handshake HTTP ", code, " : ", reponse); return(false); } return(StringFind(reponse, "\"ok\":true") >= 0); } //+------------------------------------------------------------------+ //| Nombre de positions de marché ouvertes | //| | //| OrdersTotal() compte aussi les ordres en attente ; on ne retient | //| que les positions réellement ouvertes (OP_BUY, OP_SELL). | //+------------------------------------------------------------------+ int CompterPositions() { int compte = 0; for(int i = 0; i < OrdersTotal(); i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderType() == OP_BUY || OrderType() == OP_SELL) compte++; } return(compte); } //+------------------------------------------------------------------+ //| Envoi de l'état du compte | //+------------------------------------------------------------------+ void EnvoyerEtatCompte() { // Le niveau de marge n'est pas défini quand aucune marge n'est // engagée : on l'expose à zéro plutôt que de laisser le terminal // renvoyer une valeur non initialisée. double marge = AccountMargin(); double niveauMarge = (marge > 0.0) ? (AccountEquity() / marge * 100.0) : 0.0; string corps = "{"; corps += "\"token\":\"" + JetonCompte + "\","; corps += "\"balance\":" + DoubleToString(AccountBalance(), 2) + ","; corps += "\"equity\":" + DoubleToString(AccountEquity(), 2) + ","; corps += "\"margin\":" + DoubleToString(marge, 2) + ","; corps += "\"free_margin\":" + DoubleToString(AccountFreeMargin(), 2) + ","; corps += "\"margin_level\":" + DoubleToString(niveauMarge, 2) + ","; corps += "\"open_positions\":" + IntegerToString(CompterPositions()) + ","; corps += "\"currency\":\"" + AccountCurrency() + "\","; corps += "\"leverage\":" + IntegerToString(AccountLeverage()) + ","; corps += "\"login\":\"" + IntegerToString(AccountNumber()) + "\","; corps += "\"broker\":\"" + Echapper(AccountCompany()) + "\","; corps += "\"server\":\"" + Echapper(AccountServer()) + "\""; corps += "}"; string reponse; int code = AppelHttp("POST", "/ping", corps, reponse); if(code != 200 && JournalDetaille) Print("[TheBotAnalyzer] Ping HTTP ", code, " : ", reponse); } //+------------------------------------------------------------------+ //| Envoi des transactions clôturées sur les N derniers jours | //| | //| MQL4 parcourt l'historique par position (OrderSelect en | //| MODE_HISTORY). Chaque ligne est déjà une clôture consolidée : | //| pas d'inversion de sens à faire, contrairement aux deals MT5. | //+------------------------------------------------------------------+ void EnvoyerHistorique(int jours) { datetime depuis = TimeCurrent() - (datetime)(jours * 86400); string trades = ""; int envoyes = 0; int total = OrdersHistoryTotal(); for(int i = total - 1; i >= 0 && envoyes < 400; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue; // On ignore les ordres en attente supprimés : seules les positions // de marché portent un résultat. if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue; if(OrderCloseTime() < depuis) continue; string sens = (OrderType() == OP_BUY) ? "buy" : "sell"; if(envoyes > 0) trades += ","; trades += "{"; trades += "\"ticket\":\"" + IntegerToString(OrderTicket()) + "\","; trades += "\"magic\":" + IntegerToString(OrderMagicNumber()) + ","; trades += "\"symbol\":\"" + OrderSymbol() + "\","; trades += "\"type\":\"" + sens + "\","; trades += "\"lots\":" + DoubleToString(OrderLots(), 2) + ","; trades += "\"open_time\":" + IntegerToString((long)OrderOpenTime()) + ","; trades += "\"open_price\":" + DoubleToString(OrderOpenPrice(), 5) + ","; trades += "\"close_time\":" + IntegerToString((long)OrderCloseTime()) + ","; trades += "\"close_price\":" + DoubleToString(OrderClosePrice(), 5) + ","; trades += "\"sl\":" + DoubleToString(OrderStopLoss(), 5) + ","; trades += "\"tp\":" + DoubleToString(OrderTakeProfit(), 5) + ","; trades += "\"commission\":" + DoubleToString(OrderCommission(), 2) + ","; trades += "\"swap\":" + DoubleToString(OrderSwap(), 2) + ","; trades += "\"profit\":" + DoubleToString(OrderProfit(), 2) + ","; trades += "\"state\":\"closed\","; trades += "\"comment\":\"" + Echapper(OrderComment()) + "\""; trades += "}"; envoyes++; } if(envoyes == 0) return; string corps = "{\"token\":\"" + JetonCompte + "\",\"trades\":[" + trades + "]}"; string reponse; int code = AppelHttp("POST", "/trades", corps, reponse); if(JournalDetaille) Print("[TheBotAnalyzer] ", envoyes, " transaction(s) envoyée(s), HTTP ", code); } //+------------------------------------------------------------------+ //| Envoi des positions ouvertes | //+------------------------------------------------------------------+ void EnvoyerPositionsOuvertes() { string trades = ""; int envoyes = 0; for(int i = 0; i < OrdersTotal(); i++) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue; if(envoyes > 0) trades += ","; trades += "{"; trades += "\"ticket\":\"" + IntegerToString(OrderTicket()) + "\","; trades += "\"magic\":" + IntegerToString(OrderMagicNumber()) + ","; trades += "\"symbol\":\"" + OrderSymbol() + "\","; trades += "\"type\":\"" + ((OrderType() == OP_BUY) ? "buy" : "sell") + "\","; trades += "\"lots\":" + DoubleToString(OrderLots(), 2) + ","; trades += "\"open_time\":" + IntegerToString((long)OrderOpenTime()) + ","; trades += "\"open_price\":" + DoubleToString(OrderOpenPrice(), 5) + ","; trades += "\"sl\":" + DoubleToString(OrderStopLoss(), 5) + ","; trades += "\"tp\":" + DoubleToString(OrderTakeProfit(), 5) + ","; trades += "\"swap\":" + DoubleToString(OrderSwap(), 2) + ","; trades += "\"profit\":" + DoubleToString(OrderProfit(), 2) + ","; trades += "\"state\":\"open\","; trades += "\"comment\":\"" + Echapper(OrderComment()) + "\""; trades += "}"; envoyes++; } if(envoyes == 0) return; string corps = "{\"token\":\"" + JetonCompte + "\",\"trades\":[" + trades + "]}"; string reponse; AppelHttp("POST", "/trades", corps, reponse); } //+------------------------------------------------------------------+ //| Récupération et exécution des commandes | //+------------------------------------------------------------------+ void TraiterCommandes() { string reponse; int code = AppelHttp("GET", "/commands", "", reponse); if(code != 200) return; // Extraction manuelle : MQL4 ne dispose pas d'analyseur JSON natif, // et la charge utile reste volontairement plate et prévisible. int position = 0; while(true) { int debut = StringFind(reponse, "\"id\":", position); if(debut < 0) break; long id = (long)StringToInteger(ExtraireApres(reponse, "\"id\":", debut)); string cmd = ExtraireChaine(reponse, "\"command\":\"", debut); long magic = (long)StringToInteger(ExtraireApres(reponse, "\"magic\":", debut)); if(cmd != "") { ExecuterCommande(cmd, magic); AccuserReception(id); } position = debut + 6; } } //+------------------------------------------------------------------+ void ExecuterCommande(string commande, long magic) { if(commande == "suspendre_ea") { eaSuspendu = true; AjouterMagicSuspendu(magic); Print("[TheBotAnalyzer] Suspension demandée", (magic > 0 ? " pour le magic " + IntegerToString(magic) : ""), ". Le robot concerné doit interroger GlobalVariable TheBotAnalyzer_Suspendu."); GlobalVariableSet("TheBotAnalyzer_Suspendu", 1); if(magic > 0) GlobalVariableSet("TheBotAnalyzer_Suspendu_" + IntegerToString(magic), 1); return; } if(commande == "reprendre_ea") { eaSuspendu = false; Print("[TheBotAnalyzer] Reprise demandée", (magic > 0 ? " pour le magic " + IntegerToString(magic) : ""), "."); GlobalVariableSet("TheBotAnalyzer_Suspendu", 0); if(magic > 0) GlobalVariableSet("TheBotAnalyzer_Suspendu_" + IntegerToString(magic), 0); return; } if(commande == "fermer_positions") { if(!AutoriserFermeture) { Print("[TheBotAnalyzer] Ordre de fermeture reçu mais désactivé dans les paramètres du pont."); return; } FermerPositions(magic, ""); return; } if(commande == "ping") Print("[TheBotAnalyzer] Ping serveur reçu."); } //+------------------------------------------------------------------+ //| Fermeture des positions, filtrée par magic et/ou symbole | //| | //| L'index des ordres change à chaque fermeture : on parcourt donc | //| la liste à l'envers pour ne sauter aucune position. | //+------------------------------------------------------------------+ void FermerPositions(long magic, string symbole) { int fermees = 0; for(int i = OrdersTotal() - 1; i >= 0; i--) { if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue; if(OrderType() != OP_BUY && OrderType() != OP_SELL) continue; if(magic > 0 && OrderMagicNumber() != magic) continue; if(symbole != "" && OrderSymbol() != symbole) continue; // Un ordre d'achat se solde à la vente (Bid), et inversement. double prix = (OrderType() == OP_BUY) ? MarketInfo(OrderSymbol(), MODE_BID) : MarketInfo(OrderSymbol(), MODE_ASK); if(OrderClose(OrderTicket(), OrderLots(), prix, GlissementPoints, clrNONE)) fermees++; else Print("[TheBotAnalyzer] Échec de fermeture du ticket ", OrderTicket(), " : erreur ", GetLastError()); } Print("[TheBotAnalyzer] ", fermees, " position(s) fermée(s) sur ordre du coupe-circuit."); } //+------------------------------------------------------------------+ void AccuserReception(long id) { string reponse; string corps = "{\"token\":\"" + JetonCompte + "\"}"; AppelHttp("POST", "/commands/" + IntegerToString(id) + "/ack", corps, reponse); } //+------------------------------------------------------------------+ //| Appel HTTP générique | //+------------------------------------------------------------------+ int AppelHttp(string methode, string chemin, string corps, string &reponse) { string url = ServeurURL + chemin; string entetes = "Content-Type: application/json\r\n"; entetes += "Authorization: Bearer " + JetonCompte + "\r\n"; char donnees[]; char resultat[]; string entetesReponse; if(StringLen(corps) > 0) StringToCharArray(corps, donnees, 0, StringLen(corps), CP_UTF8); ResetLastError(); int code = WebRequest(methode, url, entetes, 8000, donnees, resultat, entetesReponse); if(code == -1) { int erreur = GetLastError(); if(erreur == 4060) Print("[TheBotAnalyzer] L'adresse ", url, " n'est pas autorisée. ", "Outils → Options → Expert Advisors → Autoriser les WebRequest."); else Print("[TheBotAnalyzer] Erreur réseau ", erreur, " sur ", chemin); reponse = ""; return(-1); } reponse = CharArrayToString(resultat, 0, WHOLE_ARRAY, CP_UTF8); return(code); } //+------------------------------------------------------------------+ //| Utilitaires de lecture JSON minimalistes | //+------------------------------------------------------------------+ string ExtraireChaine(string source, string cle, int depuis) { int debut = StringFind(source, cle, depuis); if(debut < 0) return(""); debut += StringLen(cle); int fin = StringFind(source, "\"", debut); if(fin < 0) return(""); return(StringSubstr(source, debut, fin - debut)); } //+------------------------------------------------------------------+ string ExtraireApres(string source, string cle, int depuis) { int debut = StringFind(source, cle, depuis); if(debut < 0) return("0"); debut += StringLen(cle); int fin = debut; while(fin < StringLen(source)) { ushort c = StringGetCharacter(source, fin); if((c >= '0' && c <= '9') || c == '-') fin++; else break; } if(fin == debut) return("0"); return(StringSubstr(source, debut, fin - debut)); } //+------------------------------------------------------------------+ void AjouterMagicSuspendu(long magic) { if(magic <= 0) return; int taille = ArraySize(magicsSuspendus); for(int i = 0; i < taille; i++) if(magicsSuspendus[i] == magic) return; ArrayResize(magicsSuspendus, taille + 1); magicsSuspendus[taille] = magic; } //+------------------------------------------------------------------+ string Echapper(string valeur) { StringReplace(valeur, "\\", " "); StringReplace(valeur, "\"", " "); StringReplace(valeur, "\n", " "); StringReplace(valeur, "\r", " "); return(valeur); } //+------------------------------------------------------------------+