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}.`;
}