mirror of
https://github.com/adhityaalba/mt5-mtsocketapi.git
synced 2026-07-27 18:47:56 +00:00
feat: Initialize Node.js project with dotenv and a basic MT5 trading bot structure.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
.env
|
||||||
|
package-lock.json
|
||||||
|
node_modules/
|
||||||
@@ -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();
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
+41
@@ -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);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user