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;