diff --git a/bot.js b/bot.js index 95255af..8395790 100644 --- a/bot.js +++ b/bot.js @@ -2,204 +2,234 @@ require('dotenv').config(); const net = require('net'); /** - * STRUKTUR BOT TRADING DCA (SAFE MODE) - * ----------------------------------- - * 1. Basket Stop Loss: Proteksi modal total. - * 2. Max Layer: Membatasi penggunaan margin. - * 3. Lot Flat: Menghindari overload margin. - * 4. ATR-based Step: Jarak antar posisi mengikuti volatilitas pasar. + * SMART SAFE DCA BOT (v2) + * ----------------------- + * 1. Trend Filter: EMA 200 (Only BUY if Price > EMA 200). + * 2. DCA System: ATR-based steps with Max Layers protection. + * 3. Smart Exit: Closes in profit if price breaks below EMA 50 (momentum loss). + * 4. Safety: Individual SL/TP + Basket protection. */ -class SafeDCABot { +class SmartSafeBot { constructor() { this.config = { host: process.env.MT5_HOST || '127.0.0.1', port: parseInt(process.env.MT5_PORT) || 7777, symbol: process.env.SYMBOL || 'XAUUSD', lot: parseFloat(process.env.LOT_SIZE) || 0.01, - maxLayers: parseInt(process.env.MAX_LAYERS) || 5, - side: process.env.SIDE || 'BUY', - tpUsd: parseFloat(process.env.TAKE_PROFIT_USD) || 5.0, + maxLayers: parseInt(process.env.MAX_LAYERS) || 3, + orderMode: process.env.ORDER_MODE || 'MARKET', + + // Indicators + emaFast: parseInt(process.env.EMA_FAST_PERIOD) || 50, + emaSlow: parseInt(process.env.EMA_SLOW_PERIOD) || 200, + + // SL & TP (Points) + slPoints: parseInt(process.env.STOP_LOSS_POINTS) || 1000, + tpPoints: parseInt(process.env.TAKE_PROFIT_POINTS) || 2000, + + // Trailing Stop (Fallback Safety) + trailStart: parseInt(process.env.TRAILING_START_POINTS) || 500, + trailDist: parseInt(process.env.TRAILING_DISTANCE_POINTS) || 700, + trailStep: parseInt(process.env.TRAILING_STEP_POINTS) || 100, + + // Safety Basket + tpUsd: parseFloat(process.env.TAKE_PROFIT_USD) || 50.0, basketSLPercent: parseFloat(process.env.BASKET_STOP_LOSS_PERCENT) || 15.0, atrPeriod: parseInt(process.env.ATR_PERIOD) || 14, - atrMultiplier: parseFloat(process.env.ATR_MULTIPLIER) || 2.0, - minStepPoints: parseInt(process.env.MIN_STEP_POINTS) || 200 + atrMultiplier: parseFloat(process.env.ATR_MULTIPLIER) || 3.0, + minStepPoints: parseInt(process.env.MIN_STEP_POINTS) || 500, + + magic: 888 }; this.isRunning = false; - this.client = null; } - // --- KONEKSI KE MT5 --- async sendRequest(msg) { return new Promise((resolve, reject) => { const client = new net.Socket(); - client.setTimeout(5000); + client.setTimeout(10000); client.connect(this.config.port, this.config.host, () => { client.write(JSON.stringify(msg) + "\r\n"); }); client.on('data', (data) => { + const strData = data.toString(); try { - const response = JSON.parse(data.toString()); + const response = JSON.parse(strData); resolve(response); } catch (e) { - reject(new Error("Gagal parse JSON dari MT5")); + reject(new Error("JSON Parse Error: " + strData)); } client.destroy(); }); - client.on('error', (err) => { - client.destroy(); - reject(err); - }); - - client.on('timeout', () => { - client.destroy(); - reject(new Error("Timeout komunikasi dengan MT5")); - }); + client.on('error', (err) => { client.destroy(); reject(err); }); + client.on('timeout', () => { client.destroy(); reject(new Error("Timeout")); }); }); } - // --- LOGIKA UTAMA --- + async getMA(period) { + try { + const res = await this.sendRequest({ + "MSG": "MA_INDICATOR", + "SYMBOL": this.config.symbol, + "TIMEFRAME": "PERIOD_M15", + "MA_PERIOD": period, + "MA_SHIFT": 0, + "MA_METHOD": 1, // MODE_EMA + "APPLIED_PRICE": 0 // PRICE_CLOSE + }); + return res.DATA_VALUES ? res.DATA_VALUES[0] : undefined; + } catch (e) { return undefined; } + } + async tick() { if (this.isRunning) return; this.isRunning = true; try { - console.log(`\n--- [${new Date().toLocaleTimeString()}] Checking Market ---`); + console.log(`\n--- [${new Date().toLocaleTimeString()}] Monitoring Price ---`); - // 1. Ambil Status Akun + // 1. Position Sync const account = await this.sendRequest({ "MSG": "ACCOUNT_STATUS" }); - const balance = account.BALANCE; - const equity = account.EQUITY; - const marginLevel = account.MARGIN_LEVEL; - - // 2. Ambil Posisi Terbuka (Filter berdasarkan Symbol) const orders = await this.sendRequest({ "MSG": "ORDER_LIST" }); - const myOrders = (orders.ORDERS || []).filter(o => o.SYMBOL === this.config.symbol); + const myOrders = (orders.OPENED || []).filter(o => + o.SYMBOL === this.config.symbol && (o.MAGIC === this.config.magic || o.COMMENT.includes("SafePro")) + ); - // Hitung Profit/Loss Berjalan dari posisi kita - const totalPL = myOrders.reduce((sum, o) => sum + o.PROFIT, 0); + const totalPL = myOrders.reduce((sum, o) => sum + (o.PROFIT || 0), 0); const currentLayers = myOrders.length; - console.log(`Balance: ${balance} | Equity: ${equity} | Margin Level: ${marginLevel}%`); - console.log(`Posisi Terbuka (${this.config.symbol}): ${currentLayers} | Total P/L: ${totalPL.toFixed(2)} USD`); + console.log(`Equity: ${account.EQUITY} | Positions: ${currentLayers} | Floating P/L: ${totalPL.toFixed(2)} USD`); - // 3. CEK PROTECTION (Basket Stop Loss & Take Profit) - const slAmount = (balance * (this.config.basketSLPercent / 100)) * -1; - + // 2. Data Analysis + const quote = await this.sendRequest({ "MSG": "QUOTE", "SYMBOL": this.config.symbol }); + const price = quote.BID; + const emaFast = await this.getMA(this.config.emaFast); // Short trend + const emaSlow = await this.getMA(this.config.emaSlow); // Long trend + + if (price === undefined || emaFast === undefined || emaSlow === undefined) { + console.log("⚠️ Waiting for Market Data/Indicators..."); + this.isRunning = false; + return; + } + + console.log(`Price: ${price} | EMA50: ${emaFast.toFixed(2)} | EMA200: ${emaSlow.toFixed(2)}`); + + // 3. EXIT STRATEGY (Smart Close) if (currentLayers > 0) { - // Skenario A: Rugi mencapai batas % Modal - if (totalPL <= slAmount) { - console.log(`⚠️ BASKET STOP LOSS TERPICU! (Loss: ${totalPL} <= Limit: ${slAmount})`); + // A. Basket Protection + const slAmount = (account.BALANCE * (this.config.basketSLPercent / 100)) * -1; + if (totalPL <= slAmount || totalPL >= this.config.tpUsd) { + console.log(`⚠️ Basket TP/SL Goal Met! P/L: ${totalPL.toFixed(2)}. Closing all...`); await this.closeAll(myOrders); this.isRunning = false; return; } - // Skenario B: Untung mencapai target USD - if (totalPL >= this.config.tpUsd) { - console.log(`💰 TAKE PROFIT TERPICU! (Profit: ${totalPL} >= Target: ${this.config.tpUsd})`); + // B. SMART PROFIT LOCK (Exit if Momentum Slows) + // Jika total profit > $2.00 dan harga menembus ke bawah EMA 50 (Sinyal Lemah) + if (totalPL >= 2.0 && price < emaFast) { + console.log(`📉 SMART CLOSE: Momentum Melemah (Price < EMA50) & Profit Terkunci ($${totalPL.toFixed(2)}). Closing...`); await this.closeAll(myOrders); this.isRunning = false; return; } + + // C. Trailing Stop (Individual) + for (const order of myOrders) { + const pointVal = this.config.symbol.includes("XAU") ? 0.01 : 0.00001; + const profitPoints = (order.PRICE_CURRENT - order.PRICE_OPEN) * (this.config.symbol.includes("XAU") ? 100 : 100000); + + if (profitPoints >= this.config.trailStart) { + const newSL = parseFloat((order.PRICE_CURRENT - (this.config.trailDist * pointVal)).toFixed(2)); + const currentSL = order.SL || 0; + if (newSL > currentSL + (this.config.trailStep * pointVal)) { + console.log(`🛡️ Trailing SL Update Ticket #${order.TICKET} to ${newSL}`); + await this.sendRequest({ + "MSG": "ORDER_MODIFY", "TICKET": order.TICKET, "SL": newSL, "TP": order.TP + }); + } + } + } } - // 4. CEK ORDER BARU (DCA ENTRY) - if (currentLayers === 0) { - // Belum ada order, buka order pertama - console.log(`Membuka order pertama: ${this.config.side} ${this.config.lot} lot`); - await this.openOrder(); - } else if (currentLayers < this.config.maxLayers) { - // Cek apakah sudah waktunya tambah layer (DCA) - await this.checkNewLayer(myOrders); - } else { - console.log("Max layers tercapai. Menunggu harga balik atau kena Basket SL."); + // 4. ENTRY LOGIC (Buy Trend Only) + if (currentLayers < this.config.maxLayers) { + const isBullish = price > emaSlow; + + if (currentLayers === 0) { + if (isBullish) { + console.log("🚀 Sinyal Entry: Price > EMA200. Membuka posisi pertama."); + await this.openOrder(price); + } + } else { + // DCA Logic + const lastOrder = myOrders[myOrders.length - 1]; + const distPoints = Math.abs(price - lastOrder.PRICE_OPEN) * (this.config.symbol.includes("XAU") ? 100 : 100000); + + const atrRes = await this.sendRequest({ "MSG": "ATR_INDICATOR", "SYMBOL": this.config.symbol, "TIMEFRAME": "PERIOD_M15", "PERIOD": this.config.atrPeriod }); + const stepRequired = Math.max(atrRes.VALUE * this.config.atrMultiplier * (this.config.symbol.includes("XAU") ? 100 : 100000), this.config.minStepPoints); + + if (price < lastOrder.PRICE_OPEN && distPoints >= stepRequired) { + console.log(`🛠️ DCA Layer: Dist ${distPoints.toFixed(0)} >= Required ${stepRequired.toFixed(0)}. Adding Layer.`); + await this.openOrder(price); + } + } } } catch (error) { - console.error("Gagal menjalankan tick:", error.message); + console.error("Tick Error:", error.message); } finally { this.isRunning = false; } } - async openOrder() { + async openOrder(currentPrice) { + const pointVal = this.config.symbol.includes("XAU") ? 0.01 : 0.00001; + const slPrice = currentPrice - (this.config.slPoints * pointVal); + const tpPrice = currentPrice + (this.config.tpPoints * pointVal); + const cmd = { "MSG": "ORDER_SEND", "SYMBOL": this.config.symbol, - "TYPE": this.config.side === 'BUY' ? 'ORDER_TYPE_BUY' : 'ORDER_TYPE_SELL', "VOLUME": this.config.lot, - "COMMENT": "SafeDCABot_v1" + "MAGIC": this.config.magic, + "COMMENT": "SafePro_v2", + "SL": parseFloat(slPrice.toFixed(2)), + "TP": parseFloat(tpPrice.toFixed(2)) }; - const res = await this.sendRequest(cmd); - if (res.ERROR_ID === 0) console.log("✅ Order berhasil dibuka."); - else console.error("❌ Gagal buka order:", res.ERROR_DESCRIPTION); - } - async checkNewLayer(myOrders) { - // Ambil harga saat ini - const quote = await this.sendRequest({ "MSG": "QUOTE", "SYMBOL": this.config.symbol }); - const currentPrice = this.config.side === 'BUY' ? quote.BID : quote.ASK; - - // Ambil harga order terakhir - // Kita cari order yang harganya paling jauh (sesuai arah DCA) - const lastOrder = myOrders[myOrders.length - 1]; - const lastEntryPrice = lastOrder.PRICE_OPEN; - - // Hitung Jarak Aman menggunakan ATR (Average True Range) - const atrRes = await this.sendRequest({ - "MSG": "ATR_INDICATOR", - "SYMBOL": this.config.symbol, - "TIMEFRAME": "PERIOD_M1", - "PERIOD": this.config.atrPeriod - }); - - // Jarak Step = ATR * Multiplier, tapi minimal sekian Points/Pips - let stepPoints = (atrRes.VALUE * this.config.atrMultiplier) * 100000; // Konversi ke points (untuk Gold biasanya 2 digit belakang) - if (this.config.symbol.includes("XAU")) stepPoints = (atrRes.VALUE * this.config.atrMultiplier) * 100; // Adjusment xau - - const finalStep = Math.max(stepPoints, this.config.minStepPoints); - - // Hitung selisih harga saat ini dengan harga terakhir - const priceDiff = Math.abs(currentPrice - lastEntryPrice); - const diffInPoints = priceDiff * (this.config.symbol.includes("XAU") ? 100 : 100000); - - console.log(`Last Entry: ${lastEntryPrice} | Current: ${currentPrice} | Dist: ${diffInPoints.toFixed(0)} pts | Required Step: ${finalStep.toFixed(0)} pts`); - - // Jika harga bergerak melawan kita (Loss) melebihi step, baru buka layer baru - const isLossMove = this.config.side === 'BUY' ? (currentPrice < lastEntryPrice) : (currentPrice > lastEntryPrice); - - if (isLossMove && diffInPoints >= finalStep) { - console.log("Menambah layer DCA baru..."); - await this.openOrder(); + if (this.config.orderMode === 'LIMIT') { + cmd.TYPE = "ORDER_TYPE_BUY_LIMIT"; + cmd.PRICE = parseFloat((currentPrice - (150 * pointVal)).toFixed(2)); + console.log(`Memasang BUY LIMIT di ${cmd.PRICE}`); + } else { + cmd.TYPE = "ORDER_TYPE_BUY"; + console.log(`Melakukan MARKET BUY...`); } + + const res = await this.sendRequest(cmd); + if (res.ERROR_ID === 0) console.log("✅ Order Berhasil."); + else console.error("❌ Gagal Order:", res.ERROR_DESCRIPTION); } async closeAll(orders) { - console.log(`Sedang menutup ${orders.length} posisi...`); for (const order of orders) { - await this.sendRequest({ - "MSG": "ORDER_CLOSE", - "TICKET": order.TICKET - }); + await this.sendRequest({ "MSG": "ORDER_CLOSE", "TICKET": order.TICKET }); } console.log("✅ Semua posisi ditutup."); } start() { - console.log("--- SAFE DCA BOT STARTING ---"); - console.log(`Symbol: ${this.config.symbol} | Strategy: ${this.config.side} DCA`); - console.log(`Safety: Basket SL ${this.config.basketSLPercent}% | Max Layers: ${this.config.maxLayers}`); - - // Tick setiap 5 detik - setInterval(() => this.tick(), 5000); + console.log("--- SMART SAFE BOT v2 STARTED (RSI REMOVED) ---"); + setInterval(() => this.tick(), 10000); this.tick(); } } -const bot = new SafeDCABot(); +const bot = new SmartSafeBot(); bot.start();