Bot & Automation

Treding Forex Ai

/root/hermes-projects/Treding Forex Ai

dist/src/services/autoTradingService.js text
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AutoTradingService = void 0;
const safetyFilter_1 = require("./safetyFilter");
const markets_1 = require("../constants/markets");
class AutoTradingService {
    tradingService;
    broker;
    marketDataService;
    config;
    telegram;
    scanTimer = null;
    monitorTimer = null;
    scanning = false;
    monitoring = false;
    lastExecutedAt = new Map();
    safetyFilter;
    constructor(tradingService, broker, marketDataService, config, telegram) {
        this.tradingService = tradingService;
        this.broker = broker;
        this.marketDataService = marketDataService;
        this.config = config;
        this.telegram = telegram;
        this.safetyFilter = new safetyFilter_1.SafetyFilter({
            maxSpreadPips: config.maxSpreadPips,
            skipRolloverWindow: config.skipRolloverWindow,
            maxDailyLossAmount: 0,
            maxOpenPositions: config.maxOpenPositions
        });
    }
    configure(newConfig) {
        Object.assign(this.config, newConfig);
        const riskConfig = this.tradingService.riskManager.riskConfig;
        const maxDailyLossAmount = this.config.dailyLossLimitPercent
            ? riskConfig.accountBalance * (this.config.dailyLossLimitPercent / 100)
            : 0;
        this.safetyFilter.updateConfig({
            maxSpreadPips: this.config.maxSpreadPips,
            maxOpenPositions: this.config.maxOpenPositions,
            maxDailyLossAmount
        });
    }
    async emergencyStop() {
        this.stop();
        let closedCount = 0;
        try {
            const positions = await this.broker.getPositions();
            for (const pos of positions) {
                await this.broker.closePosition(pos.ticket, pos.symbolCode, pos.entryPrice);
                closedCount++;
            }
        }
        catch (error) {
            console.error("[emergency] Gagal menutup posisi:", error);
        }
        return closedCount;
    }
    start(force = false) {
        if (this.scanTimer) {
            return true;
        }
        if (force) {
            this.config.enabled = true;
        }
        if (!this.config.enabled) {
            console.log("[auto] Auto trading disabled by env.");
            return false;
        }
        if (!this.config.chatId || !this.config.userId) {
            console.warn("[auto] Disabled: AUTO_TRADE_CHAT_ID/AUTO_TRADE_USER_ID belum valid.");
            this.config.enabled = false;
            return false;
        }
        // --- Scan loop: cek sinyal & entry baru ---
        void this.runScan();
        this.scanTimer = setInterval(() => {
            void this.runScan();
        }, this.config.scanIntervalSeconds * 1000);
        // --- Monitor loop: pantau floating P&L & auto-close ---
        void this.runMonitor();
        this.monitorTimer = setInterval(() => {
            void this.runMonitor();
        }, this.config.monitorIntervalSeconds * 1000);
        console.log(`[auto] Started — symbols: ${this.config.symbols.join(", ")} | scan: ${this.config.scanIntervalSeconds}s | monitor: ${this.config.monitorIntervalSeconds}s | cooldown: ${this.config.cooldownSeconds}s`);
        return true;
    }
    stop() {
        this.config.enabled = false;
        if (this.scanTimer) {
            clearInterval(this.scanTimer);
            this.scanTimer = null;
        }
        if (this.monitorTimer) {
            clearInterval(this.monitorTimer);
            this.monitorTimer = null;
        }
        console.log("[auto] Auto trading stopped.");
    }
    isEnabled() {
        return this.config.enabled;
    }
    status() {
        return [
            `Enabled: ${this.config.enabled}`,
            `Symbols: ${this.config.symbols.join(", ")}`,
            `Mode: ${this.config.mode}`,
            `Scan interval: ${this.config.scanIntervalSeconds}s`,
            `Monitor interval: ${this.config.monitorIntervalSeconds}s`,
            `Cooldown: ${this.config.cooldownSeconds}s`,
            `Min confidence: ${this.config.minConfidence}%`,
            `Max spread: ${this.config.maxSpreadPips} pips`,
            `Skip rollover: ${this.config.skipRolloverWindow}`,
            `Chat configured: ${Boolean(this.config.chatId)}`,
            `User configured: ${Boolean(this.config.userId)}`
        ].join("\n");
    }
    // ─── Scan Loop ────────────────────────────────────────────────────────────
    async runScan() {
        if (this.scanning) {
            return;
        }
        this.scanning = true;
        try {
            for (const symbolCode of this.config.symbols) {
                await this.scanSymbol(symbolCode);
            }
        }
        finally {
            this.scanning = false;
        }
    }
    async scanSymbol(symbolCode) {
        if (!this.config.chatId || !this.config.userId) {
            return;
        }
        // 1. Cek cooldown
        if (this.isCoolingDown(symbolCode)) {
            const remaining = this.cooldownRemainingSeconds(symbolCode);
            console.log(`[scan] ${symbolCode} cooldown — ${remaining}s remaining.`);
            return;
        }
        // 2. Cek safety filter sebelum analisis
        const safetyResult = await this.runSafetyCheck(symbolCode);
        if (!safetyResult.safe) {
            console.log(`[scan] ${symbolCode} safety filter blocked: ${safetyResult.reasons.join("; ")}`);
            return;
        }
        // 3. Analisis sinyal
        try {
            const result = await this.tradingService.createSignal({
                chatId: this.config.chatId,
                userId: this.config.userId,
                symbolCode,
                mode: this.config.mode
            });
            if (!result.orderIntent) {
                console.log(`[scan] ${symbolCode} skip — ${result.signal.action} ${result.signal.confidence}% | ${result.riskNotes.join("; ")}`);
                return;
            }
            if (result.signal.confidence < this.config.minConfidence) {
                console.log(`[scan] ${symbolCode} skip — confidence ${result.signal.confidence}% < min ${this.config.minConfidence}%`);
                return;
            }
            // Terapkan penyesuaian parameter dinamis hasil setup pengguna
            const orderIntent = result.orderIntent;
            const symbol = (0, markets_1.findMarketSymbol)(symbolCode);
            if (this.config.lot !== undefined) {
                orderIntent.positionSizing.lotSize = this.config.lot;
            }
            const action = orderIntent.action;
            const entry = orderIntent.entry;
            if (this.config.stopLossPips !== undefined) {
                const stopDistance = this.config.stopLossPips * symbol.pipSize;
                orderIntent.stopLoss = action === "BUY" ? entry - stopDistance : entry + stopDistance;
                orderIntent.positionSizing.stopDistancePips = this.config.stopLossPips;
            }
            if (this.config.targetProfitPips !== undefined) {
                const tpDistance = this.config.targetProfitPips * symbol.pipSize;
                orderIntent.takeProfit1 = action === "BUY" ? entry + tpDistance : entry - tpDistance;
            }
            // Simpan perubahan pending order agar dibaca dengan benar oleh adapter broker
            await this.tradingService.store.savePendingOrder(orderIntent);
            // 4. Entry market dengan pilihan akun Demo / Real
            const trade = await this.tradingService.executePendingOrder(orderIntent.id, {
                desiredExecutionMode: this.config.accountMode
            });
            this.lastExecutedAt.set(symbolCode, Date.now());
            console.log(`[scan] ${symbolCode} ENTRY ${trade.action} @ ${trade.entry} | SL: ${trade.stopLoss} | TP1: ${trade.takeProfit1} | lot: ${trade.positionSizing.lotSize} | broker: ${trade.broker}`);
            // 5. Kirim notifikasi Telegram
            if (this.telegram && this.config.chatId) {
                await this.sendTelegramNotif(this.config.chatId, this.formatEntryNotif(trade.symbolCode, trade.action, trade.entry, trade.stopLoss, trade.takeProfit1, trade.positionSizing.lotSize, result.signal.confidence));
            }
        }
        catch (error) {
            const message = error instanceof Error ? error.message : "unknown error";
            console.error(`[scan] ${symbolCode} failed: ${message}`);
        }
    }
    // ─── Monitor Loop ──────────────────────────────────────────────────────────
    async runMonitor() {
        if (this.monitoring) {
            return;
        }
        this.monitoring = true;
        try {
            await this.monitorPositions();
        }
        finally {
            this.monitoring = false;
        }
    }
    async monitorPositions() {
        let positions;
        try {
            positions = await this.broker.getPositions();
        }
        catch (error) {
            const message = error instanceof Error ? error.message : "unknown";
            console.warn(`[monitor] getPositions failed: ${message}`);
            return;
        }
        if (positions.length === 0) {
            return;
        }
        console.log(`[monitor] Checking ${positions.length} open position(s)...`);
        for (const position of positions) {
            await this.checkPositionClose(position);
        }
    }
    async checkPositionClose(position) {
        // Ambil harga live untuk hitung floating P&L
        const symbol = (0, markets_1.findMarketSymbol)(position.symbolCode);
        if (!symbol) {
            return;
        }
        let currentPrice;
        try {
            const marketData = await this.marketDataService.getCandles(symbol, "M1", 2);
            const lastCandle = marketData.candles.at(-1);
            if (!lastCandle) {
                return;
            }
            currentPrice = lastCandle.close;
        }
        catch {
            return;
        }
        // Hitung floating P&L
        const priceDiff = position.action === "BUY"
            ? currentPrice - position.entryPrice
            : position.entryPrice - currentPrice;
        const floatingPnl = Math.round(priceDiff * position.lotSize * 100 * 100) / 100;
        console.log(`[monitor] ${position.symbolCode} ${position.action} @ ${position.entryPrice} | now: ${currentPrice} | P&L: ${floatingPnl >= 0 ? "+" : ""}${floatingPnl}`);
        // Cek apakah harga sudah mencapai TP1
        const tp1Hit = position.action === "BUY"
            ? currentPrice >= position.takeProfit1
            : currentPrice <= position.takeProfit1;
        // Cek SL hit (safety net — seharusnya broker sudah handle ini)
        const slHit = position.action === "BUY"
            ? currentPrice <= position.stopLoss
            : currentPrice >= position.stopLoss;
        if (tp1Hit || slHit) {
            const reason = tp1Hit ? "TP1 tercapai" : "SL hit";
            try {
                await this.broker.closePosition(position.ticket, position.symbolCode, currentPrice);
                console.log(`[monitor] ${position.symbolCode} CLOSED — ${reason} | P&L: ${floatingPnl >= 0 ? "+" : ""}${floatingPnl}`);
                // Kirim notifikasi Telegram
                const chatId = position.chatId || this.config.chatId;
                if (this.telegram && chatId) {
                    await this.sendTelegramNotif(chatId, this.formatCloseNotif(position.symbolCode, reason, currentPrice, floatingPnl));
                }
                // Set cooldown setelah close
                this.lastExecutedAt.set(position.symbolCode, Date.now());
            }
            catch (error) {
                const message = error instanceof Error ? error.message : "unknown";
                console.error(`[monitor] Failed to close ${position.symbolCode}: ${message}`);
            }
        }
    }
    // ─── Safety Check ──────────────────────────────────────────────────────────
    async runSafetyCheck(symbolCode) {
        const symbol = (0, markets_1.findMarketSymbol)(symbolCode);
        if (!symbol) {
            return { safe: false, reasons: [`Symbol ${symbolCode} tidak dikenal.`] };
        }
        // Estimasi spread dari data terbaru
        let estimatedSpreadPips = symbol.defaultSpreadPips;
        try {
            const marketData = await this.marketDataService.getCandles(symbol, "M5", 20);
            const candles = marketData.candles;
            if (candles.length >= 2) {
                const lastCandle = candles.at(-1);
                const atrProxy = lastCandle.high - lastCandle.low; // proxy ATR 1 candle
                estimatedSpreadPips = safetyFilter_1.SafetyFilter.estimateSpreadPips(atrProxy, symbol.pipSize, symbol.defaultSpreadPips);
            }
        }
        catch {
            // Jika gagal ambil data, pakai default spread
        }
        // Hitung open positions untuk simbol ini
        let openCount = 0;
        try {
            const positions = await this.broker.getPositions();
            openCount = positions.filter((p) => p.symbolCode === symbolCode).length;
        }
        catch {
            // Jika gagal, anggap 0
        }
        // Hitung daily loss secara dinamis dari store
        const todayLossAmount = this.config.chatId
            ? await this.tradingService.getTodayLoss(this.config.chatId)
            : 0;
        return this.safetyFilter.check(symbolCode, estimatedSpreadPips, todayLossAmount, openCount);
    }
    // ─── Cooldown ──────────────────────────────────────────────────────────────
    isCoolingDown(symbolCode) {
        const lastExecution = this.lastExecutedAt.get(symbolCode);
        if (!lastExecution) {
            return false;
        }
        return Date.now() - lastExecution < this.config.cooldownSeconds * 1000;
    }
    cooldownRemainingSeconds(symbolCode) {
        const lastExecution = this.lastExecutedAt.get(symbolCode);
        if (!lastExecution) {
            return 0;
        }
        const elapsed = (Date.now() - lastExecution) / 1000;
        return Math.max(0, Math.ceil(this.config.cooldownSeconds - elapsed));
    }
    // ─── Telegram Notif ────────────────────────────────────────────────────────
    async sendTelegramNotif(chatId, message) {
        try {
            await this.telegram?.sendMessage(chatId, message, { parse_mode: "Markdown" });
        }
        catch (error) {
            const msg = error instanceof Error ? error.message : "unknown";
            console.warn(`[auto] Telegram notif failed: ${msg}`);
        }
    }
    formatEntryNotif(symbol, action, entry, sl, tp1, lot, confidence) {
        const emoji = action === "BUY" ? "🟢" : "🔴";
        return [
            `${emoji} *AUTO ENTRY — ${symbol}*`,
            ``,
            `Action: *${action}*`,
            `Entry: \`${entry}\``,
            `Stop Loss: \`${sl ?? "-"}\``,
            `Take Profit 1: \`${tp1 ?? "-"}\``,
            `Lot Size: \`${lot}\``,
            `Confidence: \`${confidence}%\``,
            ``,
            `_Bot auto-entry berdasarkan sinyal AI. Selalu gunakan risk management._`
        ].join("\n");
    }
    formatCloseNotif(symbol, reason, closePrice, pnl) {
        const emoji = pnl >= 0 ? "✅" : "❌";
        const pnlStr = `${pnl >= 0 ? "+" : ""}${pnl.toFixed(2)}`;
        return [
            `${emoji} *AUTO CLOSE — ${symbol}*`,
            ``,
            `Reason: *${reason}*`,
            `Close Price: \`${closePrice}\``,
            `P&L: \`${pnlStr}\``,
            ``,
            `_Cooldown aktif. Bot akan scan kembali setelah ${this.config.cooldownSeconds} detik._`
        ].join("\n");
    }
}
exports.AutoTradingService = AutoTradingService;