//+------------------------------------------------------------------+ //| SignalNB_Bridge.mq5 | //| สะพานเชื่อม signalnb.com → MT5 ของคุณ (เทรดในเครื่องคุณเอง) | //| ผู้พัฒนา: ครูราเมนทร์ สมใจ (ครูอินดี้) · LINE: hfb0262q | //+------------------------------------------------------------------+ // วิธีติดตั้ง (ทำครั้งเดียว): // 1) เปิด MetaTrader 5 → เมนู Tools > Options > Expert Advisors // ติ๊ก "Allow WebRequest for listed URL" แล้วเพิ่ม: https://signalnb.com // 2) ก็อปไฟล์นี้ไปที่โฟลเดอร์ MQL5/Experts (เปิดจาก File > Open Data Folder) // แล้วคอมไพล์ใน MetaEditor (F7) // 3) ลาก EA ลงกราฟ XAUUSD ของโบรกเกอร์คุณ → วางโทเคนที่ได้จากหน้า trade.php // 4) เริ่มที่บัญชี "เดโม" ก่อนเสมอ · จะเทรดเงินจริงต้องเปิดสวิตช์ 2 ชั้น // (AllowRealTrading=true ที่นี่ + เปิด "อนุญาตเทรดเงินจริง" บนเว็บ) //------------------------------------------------------------------ // ความปลอดภัยที่ EA นี้บังคับเสมอ (ต่อให้เว็บส่งอะไรมาก็ทำเกินนี้ไม่ได้): // - เทรดเฉพาะสัญลักษณ์ที่ EA แปะอยู่ (กันส่งผิดคู่) // - ล็อตถูก clamp ด้วย MaxLotLocal และเพดานที่เว็บส่งมา (เอาค่าน้อยกว่า) // - ถ้า RequireSL แต่คำสั่งไม่มี SL → ปฏิเสธ // - บัญชีจริง + AllowRealTrading=false → ปฏิเสธทุกคำสั่ง (โหมดดูอย่างเดียว) // - Kill switch จากเว็บ → หยุดรับคำสั่งทันที //------------------------------------------------------------------ #property copyright "SignalNB · ครูอินดี้" #property version "1.00" #property strict #include input string InpToken = ""; // โทเคนจากหน้า trade.php input string InpBaseUrl = "https://signalnb.com"; // โดเมนเว็บ (ต้องตรงกับที่อนุญาตใน WebRequest) input bool AllowRealTrading = false; // สวิตช์ชั้นที่ 1: อนุญาตเทรดบัญชีจริง (ค่าเริ่ม=ปิด) input double MaxLotLocal = 1000.0; // (ไม่ใช้แล้ว — เพดานล็อตคุมจากเว็บ) เก็บไว้เพื่อความเข้ากันได้เดิม input int PollSeconds = 3; // ดึงคำสั่งทุกกี่วินาที input int MagicNumber = 990261; // เลขประจำ EA (แยกไม้ของเราออกจากไม้อื่น) input int MaxSlippagePoints = 30; // สลิปเพจสูงสุด (จุด) CTrade trade; string gUrl; datetime gLastReport = 0; int gFail = 0; //+------------------------------------------------------------------+ int OnInit() { if(StringLen(InpToken) < 24){ Alert("SignalNB Bridge: ยังไม่ได้ใส่โทเคน (ก็อปจากหน้า trade.php)"); return(INIT_FAILED); } gUrl = InpBaseUrl + "/bridge_api.php"; trade.SetExpertMagicNumber(MagicNumber); trade.SetDeviationInPoints(MaxSlippagePoints); trade.SetTypeFillingBySymbol(_Symbol); EventSetTimer(PollSeconds); PrintFormat("SignalNB Bridge เริ่มทำงาน · symbol=%s · %s", _Symbol, (AccountInfoInteger(ACCOUNT_TRADE_MODE)==ACCOUNT_TRADE_MODE_DEMO ? "บัญชีเดโม" : "บัญชีจริง")); SendReport(); return(INIT_SUCCEEDED); } void OnDeinit(const int reason){ EventKillTimer(); } //+------------------------------------------------------------------+ //| HTTP POST (x-www-form-urlencoded) → คืน body เป็น string | //+------------------------------------------------------------------+ bool HttpPost(string postData, string &outBody) { char post[], result[]; string headers = "Content-Type: application/x-www-form-urlencoded\r\n"; string rheaders; StringToCharArray(postData, post, 0, StringLen(postData), CP_UTF8); ArrayResize(post, ArraySize(post)-1); // ตัด null ท้าย ResetLastError(); int code = WebRequest("POST", gUrl, headers, 8000, post, result, rheaders); if(code == -1){ int e = GetLastError(); if(e==4014 || e==5203){ static bool warned=false; if(!warned){ Alert("SignalNB: ต้องเพิ่ม ", InpBaseUrl, " ใน Tools>Options>Expert Advisors > Allow WebRequest"); warned=true; } } return(false); } outBody = CharArrayToString(result, 0, WHOLE_ARRAY, CP_UTF8); return(code==200); } string UrlEncode(string s){ string o=""; uchar b[]; StringToCharArray(s,b,0,StringLen(s),CP_UTF8); for(int i=0;i='0'&&c<='9')||(c>='A'&&c<='Z')||(c>='a'&&c<='z')||c=='-'||c=='_'||c=='.'||c=='~') o+=CharToString(c); else o+=StringFormat("%%%02X",c); } return o; } //+------------------------------------------------------------------+ //| Timer: poll คำสั่ง + รายงานยอดเป็นระยะ | //+------------------------------------------------------------------+ void OnTimer() { string body; string post = "op=poll&token=" + UrlEncode(InpToken); if(!HttpPost(post, body)){ gFail++; if(gFail==5) Print("SignalNB: เชื่อมต่อเซิร์ฟเวอร์ไม่ได้ 5 ครั้งติด — ตรวจ WebRequest/อินเทอร์เน็ต"); return; } gFail = 0; string lines[]; int n = StringSplit(body, '\n', lines); if(n<=0) return; // บรรทัดแรก: OK|time|kill|require_sl|max_lot|allow_real string h[]; int hn = StringSplit(lines[0], '|', h); if(hn < 6 || h[0] != "OK") return; bool srvKill = (h[2]=="1"); bool srvRequireSL = (h[3]=="1"); double srvMaxLot = StringToDouble(h[4]); bool srvAllowReal = (h[5]=="1"); if(!srvKill){ for(int i=1;i=9) ? p[8] : "MARKET"; // MARKET | LIMIT double entry = (pn>=10) ? StringToDouble(p[9]) : 0.0; double ref = (pn>=11) ? StringToDouble(p[10]): 0.0; // ราคาอ้างอิงจากเว็บ (ไว้คำนวณระยะ SL/TP จากราคาเปิดจริง) ExecuteOpen(id, p[3], StringToDouble(p[4]), StringToDouble(p[5]), StringToDouble(p[6]), p[7], srvRequireSL, srvMaxLot, srvAllowReal, otype, entry, ref); } else if(op=="CLOSE"){ ExecuteClose(id, (ulong)StringToInteger(p[3])); } else if(op=="MODIFY"){ ExecuteModify(id, (ulong)StringToInteger(p[3]), StringToDouble(p[4]), StringToDouble(p[5])); } } } if(TimeCurrent()-gLastReport >= 5){ SendReport(); gLastReport=TimeCurrent(); } } //+------------------------------------------------------------------+ //| แปลรหัสผลลัพธ์ (retcode) ของ MT5 เป็นภาษาไทยที่ผู้ใช้อ่านเข้าใจ | //+------------------------------------------------------------------+ string RetcodeTH(int rc) { switch(rc){ case 10004: return "ราคาขยับ (requote) — ลองใหม่อีกครั้ง"; case 10006: return "โบรกปฏิเสธคำสั่ง"; case 10013: return "คำสั่งไม่ถูกต้อง"; case 10014: return "ปริมาณล็อตไม่ถูกต้อง"; case 10015: return "ราคาไม่ถูกต้อง"; case 10016: return "SL/TP ไม่ถูกต้อง (ผิดฝั่ง/ชิดราคาเกินไป)"; case 10017: return "การเทรดถูกปิดอยู่สำหรับคู่นี้"; case 10018: return "ตลาดปิด"; case 10019: return "เงินในบัญชีไม่พอ (margin ไม่พอ)"; case 10020: return "ราคาเปลี่ยนไปแล้ว"; case 10021: return "ไม่มีราคาเสนอ (ตลาดเงียบ/ปิด)"; case 10027: return "ปิดการเทรดอัลกอในเทอร์มินัล — กดปุ่ม 'การเทรดอัลกอ' ให้เป็นสีเขียว"; case 10030: return "โหมดเติมคำสั่ง (filling) ไม่รองรับ"; case 10031: return "ไม่มีการเชื่อมต่อกับเซิร์ฟเวอร์โบรก"; default: return "คำสั่งไม่สำเร็จ"; } } //+------------------------------------------------------------------+ //| เปิดไม้ตามคำสั่ง — ผ่านด่านความปลอดภัยครบก่อนยิงจริง | //+------------------------------------------------------------------+ void ExecuteOpen(string id, string dir, double lot, double sl, double tp, string comment, bool requireSL, double srvMaxLot, bool srvAllowReal, string otype, double entry, double ref) { // ด่าน 1: บัญชีจริงต้องเปิดสวิตช์ทั้งสองฝั่ง bool isReal = (AccountInfoInteger(ACCOUNT_TRADE_MODE)!=ACCOUNT_TRADE_MODE_DEMO); if(isReal && (!AllowRealTrading || !srvAllowReal)){ Ack(id,"REJECTED","0",0,"บัญชีจริงแต่ยังไม่เปิดอนุญาตครบสองชั้น"); return; } // ด่าน 2: ต้องมี SL ถ้ากำหนด if(requireSL && sl<=0){ Ack(id,"REJECTED","0",0,"ไม่มี SL"); return; } // ด่าน 3: เพดานล็อตคุมจากเว็บอย่างเดียว (srvMaxLot=1000 ≈ ไม่จำกัด) — เลิกผูกกับ MaxLotLocal แล้ว // (กันปัญหา MT5 จำค่า MaxLotLocal เดิม เช่น 0.50 ไว้ แล้วตัดล็อตเงียบ ๆ) double reqLot = lot; // ล็อตที่ผู้ใช้ขอ (ไว้เทียบว่าโดนตัดไหม) double cap = srvMaxLot; double vmin=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); double vmax=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); double vstep=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); if(lot>cap) lot=cap; if(vstep>0) lot=MathFloor(lot/vstep)*vstep; if(lotvmax) lot=vmax; // ถ้าล็อตที่เปิดได้จริง < ที่ขอ → เตรียมข้อความบอกสาเหตุ (ไม่ตัดเงียบ) string lotNote=""; if(lot < reqLot-0.0000001) lotNote=StringFormat("ขอ %.2f แต่เปิดได้ %.2f lot (เพดานเว็บ=%.0f · MaxLotLocal=%.2f · โบรกสูงสุด=%.2f)",reqLot,lot,srvMaxLot,MaxLotLocal,vmax); double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK), bid=SymbolInfoDouble(_Symbol,SYMBOL_BID); string cmt = (comment=="" ? "SignalNB" : "SignalNB:"+comment); // ---------- คำสั่ง LIMIT (ตั้งราคาเข้าเอง = pending) ---------- if(otype=="LIMIT" && entry>0){ // ถ้าราคาเข้าอยู่ "ผิดฝั่ง" (BUY≥ตลาด / SELL≤ตลาด) = เข้าตลาดทันทีแทน bool immediate=(dir=="BUY" && entry>=ask)||(dir=="SELL" && entry<=bid); if(!immediate){ if(dir=="BUY" && sl>0 && sl>=entry){ Ack(id,"REJECTED","0",0,"SL ผิดฝั่ง (BUY Limit)"); return; } if(dir=="SELL"&& sl>0 && sl<=entry){ Ack(id,"REJECTED","0",0,"SL ผิดฝั่ง (SELL Limit)"); return; } bool okL = (dir=="BUY") ? trade.BuyLimit(lot,entry,_Symbol,sl,tp,ORDER_TIME_GTC,0,cmt) : trade.SellLimit(lot,entry,_Symbol,sl,tp,ORDER_TIME_GTC,0,cmt); if(okL && (trade.ResultRetcode()==TRADE_RETCODE_DONE||trade.ResultRetcode()==TRADE_RETCODE_PLACED)){ Ack(id,"FILLED",(string)trade.ResultOrder(),entry,(lotNote=="")?"pending":("pending · "+lotNote)); PrintFormat("SignalNB: ตั้ง %s Limit %.2f lot @ %.2f",dir,lot,entry); } else { int rcL=(int)trade.ResultRetcode(); Ack(id,"ERROR","0",0,StringFormat("ตั้ง Limit ไม่สำเร็จ: %s (retcode %d)",RetcodeTH(rcL),rcL)); } SendReport(); return; } } // ---------- คำสั่งตลาด: เปิดก่อน (ยังไม่ใส่ SL/TP) เพื่อให้ "เข้าได้จริงเสมอ" ---------- // แล้วค่อยตั้ง SL/TP จากราคาเปิดจริง → ตัดปัญหาฟีดเว็บ≠โบรก / SL ผิดฝั่ง / ชิดราคาเกินไป ทิ้งทั้งหมด bool ok = (dir=="BUY") ? trade.Buy(lot,_Symbol,0.0,0,0,cmt) : trade.Sell(lot,_Symbol,0.0,0,0,cmt); if(!(ok && trade.ResultRetcode()==TRADE_RETCODE_DONE)){ int rc=(int)trade.ResultRetcode(); Ack(id,"ERROR","0",0,StringFormat("%s (retcode %d)",RetcodeTH(rc),rc)); SendReport(); return; } ulong tk = trade.ResultOrder(); double fill = trade.ResultPrice(); // ---- ตั้ง SL/TP จากราคาเปิดจริง + ระยะที่ผู้ใช้ตั้งใจ (จาก ref เว็บ) + บัฟเฟอร์ให้โบรกยอมรับ ---- double point = SymbolInfoDouble(_Symbol,SYMBOL_POINT); int digits= (int)SymbolInfoInteger(_Symbol,SYMBOL_DIGITS); long stpLv = SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL); double spread= ask-bid; if(spread<0) spread=0; double buf = MathMax((double)stpLv*point, spread) + 10*point; // ระยะปลอดภัยขั้นต่ำจากราคาเปิด double refPx = (ref>0 ? ref : fill); double slFinal=0, tpFinal=0; if(sl>0){ // ผู้ใช้ตั้งใจให้ SL ห่างจากราคาอ้างอิงเท่าไร → ยกระยะนั้นมาวางจากราคาเปิดจริง ฝั่งที่ถูกต้อง double d=MathAbs(refPx-sl); if(d0){ double d=MathAbs(tp-refPx); if(d0 || tpFinal>0){ slFinal = (slFinal>0)? NormalizeDouble(slFinal,digits):0.0; tpFinal = (tpFinal>0)? NormalizeDouble(tpFinal,digits):0.0; if(!trade.PositionModify(_Symbol, slFinal, tpFinal)){ // ไม้เปิดสำเร็จแล้ว แต่ตั้ง SL/TP ไม่ได้ → รายงานให้รู้ (ไม่เงียบ) ไม้ยังเปิดอยู่ ปรับเองในแอปได้ int rm=(int)trade.ResultRetcode(); Ack(id,"FILLED",(string)tk,fill, StringFormat("เปิด %s %.2f lot สำเร็จ @ %.2f แต่ตั้ง SL/TP อัตโนมัติไม่ได้ (%s) — ปรับได้ในแอป", dir,lot,fill,RetcodeTH(rm))); SendReport(); return; } } Ack(id,"FILLED",(string)tk,fill,lotNote); PrintFormat("SignalNB: เปิด %s %.2f lot @ %.2f | SL %.2f TP %.2f (ticket %I64u) %s", dir,lot,fill,slFinal,tpFinal,tk,lotNote); SendReport(); } //+------------------------------------------------------------------+ void ExecuteClose(string id, ulong ticket) { if(PositionSelectByTicket(ticket) && trade.PositionClose(ticket)){ Ack(id,"FILLED",(string)ticket,trade.ResultPrice(),""); PrintFormat("SignalNB: ปิด ticket %I64u",ticket); } else { Ack(id,"ERROR",(string)ticket,0,"ปิดไม่สำเร็จ/ไม่พบไม้"); } SendReport(); } //+------------------------------------------------------------------+ //| แก้ SL/TP ของไม้ที่เปิดอยู่ — จัดฝั่ง/ระยะให้ถูกต้องอัตโนมัติ | //+------------------------------------------------------------------+ void ExecuteModify(string id, ulong ticket, double sl, double tp) { if(!PositionSelectByTicket(ticket)){ Ack(id,"ERROR",(string)ticket,0,"ไม่พบไม้ (อาจปิดไปแล้ว)"); return; } string sym = PositionGetString(POSITION_SYMBOL); long ptype= PositionGetInteger(POSITION_TYPE); double point= SymbolInfoDouble(sym,SYMBOL_POINT); int digits=(int)SymbolInfoInteger(sym,SYMBOL_DIGITS); long stpLv= SymbolInfoInteger(sym,SYMBOL_TRADE_STOPS_LEVEL); double ask = SymbolInfoDouble(sym,SYMBOL_ASK), bid=SymbolInfoDouble(sym,SYMBOL_BID); double buf = MathMax((double)stpLv*point, ask-bid) + 10*point; // ราคาที่ใช้เทียบฝั่ง: ไม้ BUY ปิดที่ bid · ไม้ SELL ปิดที่ ask double ref = (ptype==POSITION_TYPE_BUY) ? bid : ask; // ใส่ SL/TP ตามระยะที่ผู้ใช้ตั้งใจ แต่จัดให้อยู่ฝั่งถูกต้อง + ห่างพอที่โบรกยอมรับ double slF=0, tpF=0; if(sl>0){ double d=MathAbs(ref-sl); if(d0){ double d=MathAbs(tp-ref); if(d0)?NormalizeDouble(slF,digits):0.0; tpF=(tpF>0)?NormalizeDouble(tpF,digits):0.0; if(trade.PositionModify(ticket, slF, tpF)){ Ack(id,"FILLED",(string)ticket,0,"แก้ SL/TP แล้ว"); PrintFormat("SignalNB: แก้ SL/TP ticket %I64u → SL %.2f TP %.2f",ticket,slF,tpF); } else { int rc=(int)trade.ResultRetcode(); Ack(id,"ERROR",(string)ticket,0,StringFormat("แก้ SL/TP ไม่ได้: %s (retcode %d)",RetcodeTH(rc),rc)); } SendReport(); } //+------------------------------------------------------------------+ void Ack(string id,string status,string ticket,double price,string err) { string body; string post="op=ack&token="+UrlEncode(InpToken)+"&id="+UrlEncode(id) +"&status="+status+"&ticket="+ticket +"&price="+DoubleToString(price,2)+"&error="+UrlEncode(err); HttpPost(post, body); } //+------------------------------------------------------------------+ //| ส่งยอดบัญชี + โพซิชันของเราขึ้นเว็บ (แสดงบน trade.php) | //+------------------------------------------------------------------+ void SendReport() { bool isReal = (AccountInfoInteger(ACCOUNT_TRADE_MODE)!=ACCOUNT_TRADE_MODE_DEMO); string mode = isReal ? "REAL" : "DEMO"; string guard= isReal ? (AllowRealTrading?"real-armed":"real-locked") : "demo"; string data = "EA|1.00|"+_Symbol+"\n"; data += StringFormat("ACC|%.2f|%.2f|%.2f|%s|%s|%s\n", AccountInfoDouble(ACCOUNT_BALANCE), AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_MARGIN_FREE), AccountInfoString(ACCOUNT_CURRENCY), mode, guard); for(int i=PositionsTotal()-1;i>=0;i--){ ulong tk=PositionGetTicket(i); if(!PositionSelectByTicket(tk)) continue; if(PositionGetInteger(POSITION_MAGIC)!=MagicNumber) continue; // เฉพาะไม้ของ EA นี้ data += StringFormat("POS|%I64u|%s|%s|%.2f|%.2f|%.2f|%.2f|%.2f\n", tk, PositionGetString(POSITION_SYMBOL), (PositionGetInteger(POSITION_TYPE)==POSITION_TYPE_BUY?"BUY":"SELL"), PositionGetDouble(POSITION_VOLUME), PositionGetDouble(POSITION_PRICE_OPEN), PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP), PositionGetDouble(POSITION_PROFIT)); } string body; HttpPost("op=report&token="+UrlEncode(InpToken)+"&data="+UrlEncode(data), body); } //+------------------------------------------------------------------+