commit 2fd8b72638f67fd14c0b793fd975510940ae2c27 Author: adhityaalba Date: Tue Mar 3 01:06:21 2026 +0700 feat: Initialize Node.js project with dotenv and a basic MT5 trading bot structure. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad9e5cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.env +package-lock.json +node_modules/ \ No newline at end of file diff --git a/bot.js b/bot.js new file mode 100644 index 0000000..95255af --- /dev/null +++ b/bot.js @@ -0,0 +1,205 @@ +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. + */ + +class SafeDCABot { + 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, + 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 + }; + + 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.connect(this.config.port, this.config.host, () => { + client.write(JSON.stringify(msg) + "\r\n"); + }); + + client.on('data', (data) => { + try { + const response = JSON.parse(data.toString()); + resolve(response); + } catch (e) { + reject(new Error("Gagal parse JSON dari MT5")); + } + client.destroy(); + }); + + client.on('error', (err) => { + client.destroy(); + reject(err); + }); + + client.on('timeout', () => { + client.destroy(); + reject(new Error("Timeout komunikasi dengan MT5")); + }); + }); + } + + // --- LOGIKA UTAMA --- + async tick() { + if (this.isRunning) return; + this.isRunning = true; + + try { + console.log(`\n--- [${new Date().toLocaleTimeString()}] Checking Market ---`); + + // 1. Ambil Status Akun + 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); + + // Hitung Profit/Loss Berjalan dari posisi kita + const totalPL = myOrders.reduce((sum, o) => sum + o.PROFIT, 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`); + + // 3. CEK PROTECTION (Basket Stop Loss & Take Profit) + const slAmount = (balance * (this.config.basketSLPercent / 100)) * -1; + + if (currentLayers > 0) { + // Skenario A: Rugi mencapai batas % Modal + if (totalPL <= slAmount) { + console.log(`āš ļø BASKET STOP LOSS TERPICU! (Loss: ${totalPL} <= Limit: ${slAmount})`); + 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})`); + await this.closeAll(myOrders); + this.isRunning = false; + return; + } + } + + // 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."); + } + + } catch (error) { + console.error("Gagal menjalankan tick:", error.message); + } finally { + this.isRunning = false; + } + } + + async openOrder() { + 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" + }; + 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(); + } + } + + async closeAll(orders) { + console.log(`Sedang menutup ${orders.length} posisi...`); + for (const order of orders) { + 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); + this.tick(); + } +} + +const bot = new SafeDCABot(); +bot.start(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..a087171 --- /dev/null +++ b/package.json @@ -0,0 +1,15 @@ +{ + "name": "mt5-mtsocketapi", + "version": "1.0.0", + "main": "test_mt5.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "dotenv": "^17.3.1" + } +} diff --git a/test_mt5.js b/test_mt5.js new file mode 100644 index 0000000..1f24e77 --- /dev/null +++ b/test_mt5.js @@ -0,0 +1,41 @@ +const net = require('net'); + +const HOST = '127.0.0.1'; +const PORT = 7777; + +const client = new net.Socket(); + +// Atur timeout agar tidak menunggu selamanya +client.setTimeout(5000); + +console.log(`Mencoba menghubungkan ke MTsocketAPI di ${HOST}:${PORT}...`); + +client.connect(PORT, HOST, () => { + console.log('āœ… Terhubung!'); + + // Gunakan \r\n jika \n saja tidak mempan + const msg = { "MSG": "HELP" }; + const command = JSON.stringify(msg) + "\r\n"; + + console.log('Mengirim perintah:', JSON.stringify(msg)); + client.write(command); +}); + +client.on('data', (data) => { + console.log('\nšŸ“„ RESPON DARI MT5:'); + console.log(data.toString()); + client.destroy(); // Tutup setelah dapet data +}); + +client.on('timeout', () => { + console.log('\nāŒ› Timeout: MT5 tidak merespon dalam 5 detik.'); + client.destroy(); +}); + +client.on('close', () => { + console.log('āŒ Koneksi ditutup.'); +}); + +client.on('error', (err) => { + console.error('āš ļø Error:', err.message); +});