Bot & Automation
Treding Forex Ai
/root/hermes-projects/Treding Forex Ai
dist/src/services/broker/broker.js
text
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Mt5BridgeBrokerAdapter = exports.GuardedRealBrokerAdapter = exports.PaperBrokerAdapter = void 0;
const tradingService_1 = require("../tradingService");
class PaperBrokerAdapter {
store;
constructor(store) {
this.store = store;
}
async execute(order) {
const trade = {
...order,
status: "executed",
executedAt: new Date().toISOString(),
broker: "paper",
brokerOrderId: `paper-${order.id}`
};
await this.store.appendPaperTrade(trade);
await this.store.removePendingOrder(order.id);
return trade;
}
async getExecutionStatus() {
return {
provider: "paper",
connected: true,
currentMode: "paper",
allowDemoTrading: true,
allowRealTrading: false,
detail: "Paper broker aktif. Order tidak dikirim ke MT5."
};
}
async getPositions() {
const allOpen = await this.store.listAllOpenTrades();
return allOpen.map((trade) => ({
ticket: trade.brokerOrderId,
symbolCode: trade.symbolCode,
action: trade.action,
lotSize: trade.positionSizing.lotSize,
entryPrice: trade.entry,
stopLoss: trade.stopLoss,
takeProfit1: trade.takeProfit1,
takeProfit2: trade.takeProfit2,
floatingPnl: 0, // paper mode: dihitung saat monitor dengan harga live
openedAt: trade.executedAt,
chatId: trade.chatId,
orderId: trade.id
}));
}
async closePosition(ticket, _symbolCode, currentPrice) {
const allOpen = await this.store.listAllOpenTrades();
const trade = allOpen.find((t) => t.brokerOrderId === ticket);
if (!trade) {
throw new Error(`Paper position ${ticket} tidak ditemukan.`);
}
const pnl = (0, tradingService_1.calcPnl)(trade, currentPrice);
await this.store.closeTrade(trade.id, currentPrice, pnl);
return { pnl };
}
}
exports.PaperBrokerAdapter = PaperBrokerAdapter;
class GuardedRealBrokerAdapter {
async execute() {
throw new Error("Real broker adapter belum dikonfigurasi. Tetap gunakan paper trading sampai broker dipilih dan diaudit.");
}
async getPositions() {
return [];
}
async getExecutionStatus() {
return {
provider: "guarded",
connected: false,
currentMode: "unknown",
allowDemoTrading: false,
allowRealTrading: false,
detail: "Broker real belum siap. Bridge MT5 belum dikonfigurasi penuh."
};
}
async closePosition() {
throw new Error("Real broker adapter belum dikonfigurasi.");
}
}
exports.GuardedRealBrokerAdapter = GuardedRealBrokerAdapter;
class Mt5BridgeBrokerAdapter {
bridgeUrl;
bridgeToken;
constructor(bridgeUrl, bridgeToken) {
this.bridgeUrl = bridgeUrl;
this.bridgeToken = bridgeToken;
}
async execute(order, options) {
if (options?.desiredExecutionMode) {
const status = await this.getExecutionStatus();
validateDesiredExecutionMode(status, options.desiredExecutionMode);
}
const response = await fetch(`${this.bridgeUrl.replace(/\/$/, "")}/orders`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.bridgeToken}`
},
body: JSON.stringify({
clientOrderId: order.id,
symbol: order.symbolCode,
side: order.action.toLowerCase(),
lotSize: order.positionSizing.lotSize,
entry: order.entry,
stopLoss: order.stopLoss,
takeProfit: order.takeProfit1,
metadata: {
mode: order.mode,
confidence: order.confidence,
riskLevel: order.riskLevel,
riskRewardRatio: order.riskRewardRatio
}
})
});
const payload = (await response.json().catch(() => ({})));
if (!response.ok || !payload.orderId) {
throw new Error(payload.detail ?? payload.message ?? "MT5 bridge menolak order.");
}
return {
...order,
status: "executed",
executedAt: new Date().toISOString(),
broker: payload.dryRun ? "dry_run" : payload.accountTradeMode === "demo" ? "mt5_demo" : "real",
brokerOrderId: payload.orderId
};
}
async getPositions() {
const response = await fetch(`${this.bridgeUrl.replace(/\/$/, "")}/positions`, {
headers: { Authorization: `Bearer ${this.bridgeToken}` }
});
if (!response.ok) {
const err = (await response.json().catch(() => ({ message: "unknown" })));
throw new Error(err.detail ?? err.message ?? "MT5 bridge gagal fetch positions.");
}
const payload = (await response.json());
return (payload.positions ?? []).map((p) => ({
ticket: String(p.ticket),
symbolCode: p.symbol,
action: p.side.toUpperCase() === "BUY" ? "BUY" : "SELL",
lotSize: p.volume,
entryPrice: p.openPrice,
stopLoss: p.sl,
takeProfit1: p.tp,
takeProfit2: p.tp,
floatingPnl: p.profit,
openedAt: p.openTime,
chatId: 0,
orderId: p.clientOrderId ?? ""
}));
}
async getExecutionStatus() {
const response = await fetch(`${this.bridgeUrl.replace(/\/$/, "")}/health`, {
headers: { Authorization: `Bearer ${this.bridgeToken}` }
});
const payload = (await response.json().catch(() => ({})));
if (!response.ok) {
throw new Error(payload.detail ?? payload.message ?? "MT5 bridge health gagal dibaca.");
}
const currentMode = payload.accountTradeMode === "demo" || payload.accountTradeMode === "real"
? payload.accountTradeMode
: "unknown";
return {
provider: "mt5_bridge",
connected: Boolean(payload.mt5Connected),
currentMode,
allowDemoTrading: Boolean(payload.allowDemoTrading) && !payload.dryRun,
allowRealTrading: Boolean(payload.allowRealTrading) && !payload.dryRun,
accountServer: payload.accountServer,
accountLoginLast4: payload.accountLoginLast4,
detail: buildBridgeStatusDetail({
connected: Boolean(payload.mt5Connected),
currentMode,
dryRun: Boolean(payload.dryRun),
allowDemoTrading: Boolean(payload.allowDemoTrading),
allowRealTrading: Boolean(payload.allowRealTrading),
accountServer: payload.accountServer,
accountLoginLast4: payload.accountLoginLast4
})
};
}
async closePosition(ticket, symbolCode,
// currentPrice tidak digunakan — MT5 akan menggunakan bid/ask live dari terminal
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_currentPrice) {
const response = await fetch(`${this.bridgeUrl.replace(/\/$/, "")}/positions/${ticket}/close`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${this.bridgeToken}`
},
body: JSON.stringify({ symbol: symbolCode })
});
const payload = (await response.json().catch(() => ({})));
if (!response.ok) {
throw new Error(payload.detail ?? payload.message ?? "MT5 bridge gagal close position.");
}
return { pnl: payload.pnl ?? 0 };
}
}
exports.Mt5BridgeBrokerAdapter = Mt5BridgeBrokerAdapter;
function validateDesiredExecutionMode(status, desiredMode) {
if (!status.connected) {
throw new Error("Bridge MT5 belum terhubung. Jalankan bridge dan tunnel terlebih dahulu.");
}
if (desiredMode === "demo") {
if (!status.allowDemoTrading) {
throw new Error("Mode demo belum aktif di bridge MT5.");
}
if (status.currentMode !== "demo") {
throw new Error(`Bridge MT5 saat ini tidak terhubung ke akun demo. ${status.detail}`);
}
return;
}
if (!status.allowRealTrading) {
throw new Error("Mode realtime masih dikunci di bridge MT5.");
}
if (status.currentMode !== "real") {
throw new Error(`Bridge MT5 saat ini tidak terhubung ke akun real. ${status.detail}`);
}
}
function buildBridgeStatusDetail(params) {
if (!params.connected) {
return "MT5 belum connected.";
}
const modeLabel = params.currentMode === "unknown" ? "mode akun tidak diketahui" : `akun ${params.currentMode}`;
const serverLabel = params.accountServer ? ` server ${params.accountServer}` : "";
const loginLabel = params.accountLoginLast4 ? ` login ${params.accountLoginLast4}` : "";
const guardLabel = `demo=${params.allowDemoTrading && !params.dryRun}, real=${params.allowRealTrading && !params.dryRun}`;
return `Bridge aktif pada ${modeLabel}${serverLabel}${loginLabel}. Gate: ${guardLabel}.`;
}