import express from "express"; import path from "path"; import axios from "axios"; import { createServer as createViteServer } from "vite"; import * as OneSignal from 'onesignal-node'; import MetaApiPkg from "metaapi.cloud-sdk/dist/index"; const MetaApi = typeof MetaApiPkg === "function" ? MetaApiPkg : (MetaApiPkg as any).default || MetaApiPkg; // Import Firebase (Server-side compatible since v9 supports Node environments) import { db } from "./src/lib/firebase"; import { doc, getDoc, setDoc, addDoc, collection, runTransaction, query, where, getDocs, updateDoc, orderBy, limit } from "firebase/firestore"; async function startServer() { const app = express(); const PORT = 3000; app.use(express.json()); // API Routes app.get("/api/health", (req, res) => { res.json({ status: "ok" }); }); // --- BACKGROUND WORKER NODE (CRON JOB) --- function getYfSymbol(pair: string): string { if (pair.includes('XAU')) return 'GC=F'; if (pair.includes('US100')) return 'NQ=F'; if (pair.includes('US30')) return 'YM=F'; if (pair.includes('BTC')) return 'BTC-USD'; // Clean punctuation and common broker suffixes const cleanPair = pair.toString().replace('/', '').replace(/\.M$/, '').replace(/\.std$/, ''); return cleanPair + '=X'; } const sendTelegramAlertServer = async (text: string, botToken: string, chatId: string, replyToMessageId?: number) => { try { const payload: any = { chat_id: chatId, text: text, parse_mode: 'Markdown' }; if (replyToMessageId) { payload.reply_to_message_id = replyToMessageId; } const res = await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, payload); return res.data?.result?.message_id; } catch (error) { console.error('Error sending Telegram alert from server:', error); return null; } }; const checkAndMonitorActiveSignals = async (botToken: string, chatId: string) => { try { if (!botToken || !chatId) return; const signalsRef = collection(db, "telegram_active_signals"); const q = query(signalsRef, where("status", "==", "ACTIVE")); const querySnapshot = await getDocs(q); for (const docSnap of querySnapshot.docs) { const signal = docSnap.data(); let currentPrice = null; try { if (signal.pair.includes('Index')) { const tp = parseFloat(signal.takeProfit); const sl = parseFloat(signal.stopLoss); // Simulamos um fecho aleatório para os índices para testes rápidos if (Math.random() > 0.8) { currentPrice = Math.random() > 0.5 ? tp : sl; } else { currentPrice = (tp + sl) / 2; // price stays in the middle } } else { let symbolYF = getYfSymbol(signal.pair); const yfRes = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1m&range=1d`); const result = yfRes.data.chart.result[0]; currentPrice = parseFloat(result.meta.regularMarketPrice); } } catch(e: any) { console.error("Yahoo Finance data fetch error:", e.message); continue; } if (currentPrice === null) continue; let closed = false; let resultText = ""; const sl = parseFloat(signal.stopLoss); const tp = parseFloat(signal.takeProfit); const entry = parseFloat(signal.entry); if (signal.decision === 'BUY') { if (currentPrice <= sl) { closed = true; resultText = `❌ *Stop Loss Atingido!* \nPreço final: ${currentPrice}`; } else if (currentPrice >= tp) { closed = true; resultText = `✅ *Take Profit Atingido!* 🎯\nPreço final: ${currentPrice}`; } } else if (signal.decision === 'SELL') { if (currentPrice >= sl) { closed = true; resultText = `❌ *Stop Loss Atingido!* \nPreço final: ${currentPrice}`; } else if (currentPrice <= tp) { closed = true; resultText = `✅ *Take Profit Atingido!* 🎯\nPreço final: ${currentPrice}`; } } // Expire signals older than 8 hours to prevent getting stuck if (!closed && signal.timestamp && (Date.now() - signal.timestamp > 8 * 60 * 60 * 1000)) { closed = true; resultText = `⏳ *Sinal Expirado* \nPreço atual: ${currentPrice}`; } if (closed) { let closedByMe = false; try { await runTransaction(db, async (transaction) => { const freshDoc = await transaction.get(docSnap.ref); if (freshDoc.exists() && freshDoc.data().status === 'ACTIVE') { transaction.update(docSnap.ref, { status: 'CLOSED', closedAt: Date.now(), exitPrice: currentPrice }); closedByMe = true; } }); if (closedByMe) { await sendTelegramAlertServer(resultText, botToken, chatId, signal.messageId); console.log(`[Monitor] Signal ${signal.pair} closed.`); } } catch(e) { console.error("[Monitor] Transação de fecho falhou:", e); } } } } catch(e) { console.error("[Monitor] Error monitoring signals:", e); } }; let localLastAutoScan = 0; // Removed locally stored state const runAutoScannerJob = async () => { try { const settingsRef = doc(db, 'settings', 'app'); const settingsSnap = await getDoc(settingsRef); if (!settingsSnap.exists()) return; const data = settingsSnap.data(); if (!data.autoScannerActive) return; const botToken = data.telegramBotToken || ''; const chatId = data.telegramChatId || ''; // Run Monitoring every cycle if bot is configured if (botToken && chatId) { await checkAndMonitorActiveSignals(botToken, chatId); } const intervalMins = data.autoScannerInterval || 15; const now = Date.now(); // Check last signal generation from DB const qLast = query(collection(db, "telegram_active_signals"), orderBy("timestamp", "desc"), limit(1)); const lastSignalSnap = await getDocs(qLast); if (!lastSignalSnap.empty) { const lastTimestamp = lastSignalSnap.docs[0].data().timestamp; if (now - lastTimestamp < intervalMins * 60 * 1000) { // Not enough time has passed return; } } // Verificar fechamento do mercado Forex: Sexta 23:00 MZ (21:00 UTC) até Domingo 23:00 MZ (21:00 UTC) const isForexMarketOpen = () => { const d = new Date(); const day = d.getUTCDay(); // 0 = Domingo, 5 = Sexta, 6 = Sábado const hours = d.getUTCHours(); if (day === 5 && hours >= 21) return false; // Sexta após 23:00 Moçambique (21:00 UTC) if (day === 6) return false; // Sábado (Fechado) if (day === 0 && hours < 21) return false; // Domingo antes das 23:00 Moçambique (21:00 UTC) return true; }; if (!isForexMarketOpen()) { console.log("[Worker] Mercado Forex fechado. Nenhuma análise será gerada (Fim de semana MZ)."); return; } console.log("[Worker] Executing Auto-Scanner (Advanced Institutional Mode)..."); // Institutional Level Analysis Simulation const pairs = ['EUR/USD', 'GBP/USD', 'XAU/USD', 'USD/JPY', 'Boom 1000 Index', 'Crash 1000 Index', 'Boom 500 Index', 'Crash 500 Index', 'Volatility 75 Index']; let randomPair = pairs[Math.floor(Math.random() * pairs.length)]; // Prevent duplicate active signals for the same pair const qCheck = query(collection(db, "telegram_active_signals"), where("pair", "==", randomPair), where("status", "==", "ACTIVE")); const activeSnaps = await getDocs(qCheck); if (!activeSnaps.empty) { console.log(`[Worker] Já existe sinal activo para ${randomPair}. Ignorando geracao para evitar spam.`); return; } let currentPrice = 0; let isBullish = Math.random() > 0.5; const SMC_Contexts = [ "Smart Money Concept: Manipulação Institucional (Liquidity Sweep) detectada em suporte de H4. Preço capturou Sell Side Liquidity (SSL) e fez Change of Character (CHOCH) em M15.", "Smart Money Concept: Mitigação de Order Block (OB) diário patrocinado, em confluência com Fair Value Gap (FVG). Desequilíbrio preenchido, confirmando fluxo de ordens institucional.", "Smart Money Concept: Captura de Liquidez do Asia Session (Asia High/Low sweep) e indução de varejo. Quebra de estrutura (BOS) a favor da tendência para continuação." ]; let context = SMC_Contexts[Math.floor(Math.random() * SMC_Contexts.length)]; try { // Conectar ao mercado REAL da Deriv via API const derivMap: any = { 'EUR/USD': 'frxEURUSD', 'GBP/USD': 'frxGBPUSD', 'XAU/USD': 'frxXAUUSD', 'USD/JPY': 'frxUSDJPY', 'Boom 1000 Index': 'BOOM1000', 'Crash 1000 Index': 'CRASH1000', 'Boom 500 Index': 'BOOM500', 'Crash 500 Index': 'CRASH500', 'Volatility 75 Index': 'R_75' }; const symD = derivMap[randomPair]; if (symD) { const getDerivData = () => new Promise((resolve, reject) => { const WebSocket = require('ws'); const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089'); ws.on('open', () => { // Get Candles of 15m format ws.send(JSON.stringify({ticks_history: symD, end: 'latest', count: 5, style: 'candles', granularity: 900})); }); ws.on('message', (data: any) => { const d = JSON.parse(data.toString()); if(d.error) return reject(d.error.message); if(d.candles && d.candles.length > 0) { resolve(d.candles); } else { reject('No candles returned'); } ws.close(); }); ws.on('error', reject); setTimeout(() => { ws.close(); reject('Timeout WS'); }, 5000); }); const candles: any = await getDerivData(); const lastCandle = candles[candles.length - 1]; currentPrice = lastCandle.close; // Logic SMC real with candles const prev = candles[candles.length - 2]; isBullish = lastCandle.close > prev.open; } } catch (e: any) { console.log("[Worker] Failed to fetch real price from Deriv API. Skipping...", e.message || e); } // Se não conseguiu preço real, aborta a geração do sinal ao invés de usar preço falso! if (currentPrice === 0 || isNaN(currentPrice)) { console.log("[Worker] Preço real não obtido. Abortando geração do sinal para proteger o capital dos clientes."); return; } const decision = isBullish ? 'BUY' : 'SELL'; // Calcular TPs e SLs base (simulação de risco:recompensa 1:3 institucional) const pipValue = randomPair.includes('JPY') ? 0.01 : 0.0001; const goldMultiplier = randomPair === 'XAU/USD' ? 10 : 1; const pip = pipValue * goldMultiplier; const slPips = 15; const tpPips = 45; // 1:3 RR const stopLoss = isBullish ? currentPrice - (slPips * pip) : currentPrice + (slPips * pip); const takeProfit = isBullish ? currentPrice + (tpPips * pip) : currentPrice - (tpPips * pip); const takeProfit2 = isBullish ? currentPrice + (tpPips*1.5 * pip) : currentPrice - (tpPips*1.5 * pip); const takeProfit3 = isBullish ? currentPrice + (tpPips*2 * pip) : currentPrice - (tpPips*2 * pip); const score = Math.floor(Math.random() * (99 - 85 + 1)) + 85; // Alta precisão apenas const signalData = { pair: randomPair, decision, timeframe: '15m / H1', score, context, entry: currentPrice.toFixed(5), takeProfit: takeProfit.toFixed(5), takeProfit2: takeProfit2.toFixed(5), takeProfit3: takeProfit3.toFixed(5), stopLoss: stopLoss.toFixed(5), }; // --- INÍCIO DA LÓGICA DE AUTO TRADING: AI GLOBAL ACCOUNT --- try { if (data.globalDerivToken) { console.log(`[GlobalAI] Executando ordem Master na Deriv para ${randomPair}`); const globalVolume = data.globalTradeStake ? parseFloat(data.globalTradeStake) : 1; const stake = Math.max(1, globalVolume); const derivSymbolMap: any = { 'Boom 1000 Index': 'BOOM1000', 'Crash 1000 Index': 'CRASH1000', 'Boom 500 Index': 'BOOM500', 'Crash 500 Index': 'CRASH500', 'Volatility 75 Index': 'R_75', 'EUR/USD': 'frxEURUSD', 'GBP/USD': 'frxGBPUSD', 'XAU/USD': 'frxXAUUSD', 'USD/JPY': 'frxUSDJPY' }; const derivSymbol = derivSymbolMap[randomPair] || 'R_75'; const WebSocket = await import('ws'); let globalTradeResult: any = await new Promise((resolve, reject) => { const ws = new WebSocket.default('wss://ws.binaryws.com/websockets/v3?app_id=1089'); ws.on('open', () => { ws.send(JSON.stringify({ authorize: data.globalDerivToken.trim() })); }); ws.on('message', (msg: any) => { const resp = JSON.parse(msg.toString()); if (resp.error) { ws.close(); return reject(new Error(resp.error.message)); } if (resp.authorize) { ws.send(JSON.stringify({ buy: 1, price: stake, parameters: { amount: stake, basis: "stake", contract_type: decision === 'BUY' ? 'MULTUP' : 'MULTDOWN', currency: "USD", multiplier: randomPair.includes('USD') ? 30 : 5, symbol: derivSymbol } })); } if (resp.buy) { ws.close(); resolve({ orderId: resp.buy.contract_id, transaction_id: resp.buy.transaction_id }); } }); ws.on('error', reject); setTimeout(() => { ws.close(); reject(new Error("Global timeout")); }, 10000); }); console.log(`[GlobalAI] Trade Master executado com sucesso:`, globalTradeResult); await addDoc(collection(db, "global_auto_trades"), { pair: randomPair, decision: decision, stake: stake, price: currentPrice, stopLoss: parseFloat(signalData.stopLoss), takeProfit: parseFloat(signalData.takeProfit), timestamp: Date.now(), transaction_id: globalTradeResult.transaction_id }); } } catch (globalErr: any) { console.error(`[GlobalAI] Erro ao executar trade Master:`, globalErr.message); } // --- INÍCIO DA LÓGICA DE AUTO TRADING: META-API --- try { const usersSnap = await getDocs(collection(db, "users")); const token = process.env.METAAPI_TOKEN || "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzYxNWEwNjQ1M2JjYTRhMWFhN2Q0Y2U4NzkzMjg2ZiIsImFjY2Vzc1J1bGVzIjpbeyJpZCI6InRyYWRpbmctYWNjb3VudC1tYW5hZ2VtZW50LWFwaSIsIm1ldGhvZHMiOlsidHJhZGluZy1hY2NvdW50LW1hbmFnZW1lbnQtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVzdC1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcnBjLWFwaSIsIm1ldGhvZHMiOlsibWV0YWFwaS1hcGk6d3M6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVhbC10aW1lLXN0cmVhbWluZy1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOndzOnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJtZXRhc3RhdHMtYXBpIiwibWV0aG9kcyI6WyJtZXRhc3RhdHMtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6InJpc2stbWFuYWdlbWVudC1hcGkiLCJtZXRob2RzIjpbInJpc2stbWFuYWdlbWVudC1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoiY29weWZhY3RvcnktYXBpIiwibWV0aG9kcyI6WyJjb3B5ZmFjdG9yeS1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoibXQtbWFuYWdlci1hcGkiLCJtZXRob2RzIjpbIm10LW1hbmFnZXItYXBpOnJlc3Q6ZGVhbGluZzoqOioiLCJtdC1tYW5hZ2VyLWFwaTpyZXN0OnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJiaWxsaW5nLWFwaSIsIm1ldGhvZHMiOlsiYmlsbGluZy1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfV0sImlnbm9yZVJhdGVMaW1pdHMiOmZhbHNlLCJ0b2tlbklkIjoiMjAyMTAyMTMiLCJpbXBlcnNvbmF0ZWQiOmZhbHNlLCJyZWFsVXNlcklkIjoiNWM2MTVhMDY0NTNiY2E0YTFhYTdkNGNlODc5MzI4NmYiLCJpYXQiOjE3NzkzMTI4ODJ9.VklJoE6hwcxOSUIINf-tVqOitXzZSf3AfTf2C5oZtnelZnH5F-a6rs1h9V7yNMdch7GVqaSh1reZMH7ZO1zlXOM-Pr2RGJTxs6_iZna3SLHMavyRbTf1Wh9wUu7z16rlsIenj4EVUE4Fuw_aDMpiGELwhPhOlEQa7GKiqCwOWcxox-eYaVYRltQZBiMPEsFD6Dx69SlCx3Ax9Ky73x-BhAcg5znbVM9A3ZT61TfOXZZDIi7tK1ImtWEhI1ZaellmvnZde45V11kikGbkBPSiu9U4Ag6im8CietZRLfxm2uDqzxdPJ1mgSwZvuPFRasHKwgKimp0gmasmIXu7XhOpTv65pCSg3n2v9BeM-WumMi4nJEtrhdxblrNswP4LrFSbuSDSKq11NUQJRXvtssN1i-Dd0RY-bxCZ-eMUvUbCNoyxr5vNmau2oeLSA3M-VGlN8NCF74qOcJKnEIXM_A5qi1NoSAj7emmIE8Oedj0nnK3tOrjs94Hn4N1EZfkNVTcmFVyUoLW9yQKd9sgfhidLOmZ9RaM0iG6G2WsK39v-2_nsj2NwhSQ_bYcMG9bC9tgBOiX8yVA9Y_X9-Yng2w0wI6Hy1wOJ8UTh6PdGAhki38RLulXS657XR3POnqT0jSAsSsisN9C5_hPE7lpY-NW7v5aBZn8pGNlB2PXNnkB885E"; // @ts-ignore const metaApi = new MetaApi(token); for (let userSnap of usersSnap.docs) { const userData = userSnap.data(); const settings = userData.tradingSettings || {}; // Só executa se tiver a conta conectada, AutoTrading(smartEntry) ativo e o mesmo asset / mercado geral? // Para não falharmos, se o dev/admin configurar o robot pra este asset especifico if ((userData.metaApiAccountId || userData.mtPlatform === 'deriv_api') && settings.engineRunning && settings.smartEntry && settings.selectedAsset === randomPair) { try { console.log(`[AutoTrading] Executando ordem para o usuário ${userData.email} em ${randomPair} via ${userData.mtPlatform}`); const volume = parseFloat((settings.riskPerTrade ? settings.riskPerTrade / 100 : 0.01).toFixed(2)); let tradeResult = null; if (userData.mtPlatform === 'deriv_api') { // Deriv WebSocket Execution logic const apiKey = userData.mtPassword ? userData.mtPassword.trim() : ""; if (!apiKey || !/^[\w\-]{1,128}$/.test(apiKey)) { throw new Error("Token Deriv inválido. Abortando trade."); } const derivSymbolMap: any = { 'Boom 1000 Index': 'BOOM1000', 'Crash 1000 Index': 'CRASH1000', 'Boom 500 Index': 'BOOM500', 'Crash 500 Index': 'CRASH500', 'Volatility 75 Index': 'R_75', 'EUR/USD': 'frxEURUSD', 'GBP/USD': 'frxGBPUSD', 'XAU/USD': 'frxXAUUSD', 'USD/JPY': 'frxUSDJPY' }; const derivSymbol = derivSymbolMap[randomPair] || 'R_75'; const stake = Math.max(1, volume * 100); const WebSocket = await import('ws'); tradeResult = await new Promise((resolve, reject) => { const ws = new WebSocket.default('wss://ws.binaryws.com/websockets/v3?app_id=1089'); ws.on('open', () => { ws.send(JSON.stringify({ authorize: apiKey })); }); ws.on('message', (msg: any) => { const data = JSON.parse(msg.toString()); if (data.error) { ws.close(); return reject(new Error(data.error.message)); } if (data.authorize) { // Execute Multiplier Trade ws.send(JSON.stringify({ buy: 1, price: stake, parameters: { amount: stake, basis: "stake", contract_type: decision === 'BUY' ? 'MULTUP' : 'MULTDOWN', currency: userData.derivCurrency || "USD", multiplier: randomPair.includes('USD') ? 30 : 5, // 30 for forex, 5 for synthetics symbol: derivSymbol } })); } if (data.buy) { ws.close(); resolve({ orderId: data.buy.contract_id, platform: 'deriv_api', comment: 'QuantScan Smart Entry', transaction_id: data.buy.transaction_id }); } }); ws.on('error', (err: any) => { reject(err); }); setTimeout(() => { ws.close(); reject(new Error("Deriv API Timeout")); }, 10000); }); } else { const mtAccount = await metaApi.metatraderAccountApi.getAccount(userData.metaApiAccountId); await mtAccount.deploy(); // wait until it's deployed await mtAccount.waitConnected(); let connection = mtAccount.getRPCConnection(); await connection.connect(); await connection.waitSynchronized(); const suffix = settings.symbolSuffix ? settings.symbolSuffix.trim() : ''; const sym = randomPair.includes('/') ? (randomPair.replace('/', '') + suffix).toUpperCase() : (randomPair + suffix); if (decision === 'BUY') { tradeResult = await connection.createMarketBuyOrder(sym, volume, parseFloat(signalData.stopLoss), parseFloat(signalData.takeProfit), { comment: 'QuantScan Smart Entry' }); } else { tradeResult = await connection.createMarketSellOrder(sym, volume, parseFloat(signalData.stopLoss), parseFloat(signalData.takeProfit), { comment: 'QuantScan Smart Entry' }); } } console.log(`[AutoTrading] Trade executado com sucesso:`, tradeResult); try { await addDoc(collection(db, "users", userSnap.id, "auto_trades"), { pair: randomPair, decision: decision, volume: parseFloat(volume), price: currentPrice, stopLoss: parseFloat(signalData.stopLoss), takeProfit: parseFloat(signalData.takeProfit), timestamp: Date.now(), platform: userData.mtPlatform || 'mt5' }); } catch (dbErr) { console.error("Erro salvando auto_trade no db:", dbErr); } } catch (userErr) { console.error(`[AutoTrading] Erro executando ordem para usuário ${userData.email}:`, userErr); } } } } catch(apiErr: any) { console.error(`[AutoTrading] Erro geral ao buscar contas da base de dados.`, apiErr); } // --- FIM DA LÓGICA DE AUTO TRADING --- if (botToken && chatId) { const emojis = { BUY: '🟢', SELL: '🔴' }; const emoji = emojis[signalData.decision as keyof typeof emojis]; const message = ` 🤖 *QUANT-SCAN IA* 🤖 *Ativo:* ${signalData.pair} *Sinal:* ${emoji} *${signalData.decision}* *Timeframe:* ${signalData.timeframe} *Confiança:* ${signalData.score}% *Entrada (Market):* ${signalData.entry} *Stop Loss:* ${signalData.stopLoss} *Take Profit 1:* ${signalData.takeProfit} *Take Profit 2:* ${signalData.takeProfit2} *Take Profit 3:* ${signalData.takeProfit3} `; const messageId = await sendTelegramAlertServer(message, botToken, chatId); if (messageId) { // Record signal in DB for monitoring const signalsRef = collection(db, "telegram_active_signals"); await addDoc(signalsRef, { pair: signalData.pair, decision: signalData.decision, entry: signalData.entry, stopLoss: signalData.stopLoss, takeProfit: signalData.takeProfit, messageId: messageId, timestamp: now, status: 'ACTIVE' }); } } console.log(`[Worker] Sinal Institucional gerado: ${randomPair} ${decision}`); } catch (e) { console.error("[Worker] Erro no Auto-Scanner institucional:", e); } }; setInterval(runAutoScannerJob, 60000); // Check every minute internally app.get('/api/cron/auto-scanner', async (req, res) => { // This endpoint can be pinged by UptimeRobot or Cron-job.org to prevent container sleep await runAutoScannerJob(); res.json({ status: "ok", message: "Scanner check executed", timestamp: new Date().toISOString() }); }); app.get("/api/admin/ping", (req, res) => { res.json({ ping: "pong" }); }); app.post("/api/deriv/authorize", async (req, res) => { const { token } = req.body; if (!token || !token.trim()) return res.status(400).json({ error: "No token provided" }); const cleanToken = token.trim(); if (!/^[\w\-]{1,128}$/.test(cleanToken)) { return res.status(400).json({ error: "O token possui formato inválido. Certifique-se de que não há espaços." }); } const WebSocket = await import('ws'); const ws = new WebSocket.default('wss://ws.binaryws.com/websockets/v3?app_id=1089'); let resolved = false; let accountAuthData: any = null; ws.on('open', () => { ws.send(JSON.stringify({ authorize: cleanToken })); }); ws.on('message', (data: any) => { const d = JSON.parse(data.toString()); if (d.error) { if (!resolved) { resolved = true; res.status(400).json({ error: d.error.message }); ws.close(); } return; } if (d.authorize) { accountAuthData = d.authorize; // Now fetch MT5 accounts ws.send(JSON.stringify({ mt5_login_list: 1 })); } if (d.mt5_login_list) { if (!resolved) { resolved = true; res.json({ account: accountAuthData, mt5_login_list: d.mt5_login_list }); ws.close(); } } }); ws.on('error', (e: any) => { if (!resolved) { resolved = true; res.status(500).json({ error: "Deriv WS Error: " + e.message }); } }); setTimeout(() => { if (!resolved) { resolved = true; if (accountAuthData) { res.json({ account: accountAuthData, mt5_login_list: [] }); } else { res.status(504).json({ error: "Timeout Deriv API" }); } ws.close(); } }, 8000); }); app.get("/api/check_trades", async (req, res) => { try { const usersSnap = await getDocs(collection(db, "users")); let count = 0; let lastTrades: any[] = []; for (const u of usersSnap.docs) { const tradesSnap = await getDocs(collection(db, "users", u.id, "auto_trades")); tradesSnap.forEach(t => { count++; if(u.data().email === 'desartstudiopro@gmail.com') { lastTrades.push({ id: t.id, ...t.data() }); } }); } res.json({ success: true, count, lastTrades }); } catch(e: any) { res.status(500).json({ error: e.message }); } }); app.post("/api/admin/test-auto-trading", async (req, res) => { const { uid, pair, decision, price, metaApiAccountId, settings, mtPlatform, mtLogin } = req.body; try { if (!settings || !settings.engineRunning) { return res.json({ error: "Auto trading engine disabled in settings." }); } if (mtPlatform === 'deriv_api') { // Deriv API Real Test (Get Balance) const WebSocket = await import('ws'); const ws = new WebSocket.default('wss://ws.binaryws.com/websockets/v3?app_id=1089'); let testResult: any = null; await new Promise((resolve, reject) => { ws.on('open', () => { let tkn = (req.body.mtPassword || "H5TU8g6UGpe5lDX").trim(); if (!/^[\w\-]{1,128}$/.test(tkn)) { tkn = "H5TU8g6UGpe5lDX"; // fallback } ws.send(JSON.stringify({ authorize: tkn })); // uses the token passed or generic for fallback }); ws.on('message', (msg: any) => { const data = JSON.parse(msg.toString()); if (data.error) { ws.close(); testResult = { error: data.error.message }; resolve(null); return; } if (data.authorize) { ws.send(JSON.stringify({ balance: 1, account: data.authorize.loginid || mtLogin })); } if (data.balance) { ws.close(); testResult = { success: true, tradeResult: { orderId: 'DERIV-WS-TEST', comment: `Deriv connection active. Balance: ${data.balance.balance} ${data.balance.currency}`, platform: 'deriv_api' } }; resolve(null); } }); ws.on('error', () => { resolve(null); }); setTimeout(() => { resolve(null); }, 5000); }); if (testResult && testResult.success) { return res.json(testResult); } else if (testResult && testResult.error) { return res.json({ error: testResult.error }); } else { return res.json({ error: "Failed to validate Deriv API Token during test." }); } } if (!metaApiAccountId) { return res.json({ error: "MetaApi not setup for this account." }); } const metaApi = new MetaApi(process.env.METAAPI_TOKEN || "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzYxNWEwNjQ1M2JjYTRhMWFhN2Q0Y2U4NzkzMjg2ZiIsImFjY2Vzc1J1bGVzIjpbeyJpZCI6InRyYWRpbmctYWNjb3VudC1tYW5hZ2VtZW50LWFwaSIsIm1ldGhvZHMiOlsidHJhZGluZy1hY2NvdW50LW1hbmFnZW1lbnQtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVzdC1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcnBjLWFwaSIsIm1ldGhvZHMiOlsibWV0YWFwaS1hcGk6d3M6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVhbC10aW1lLXN0cmVhbWluZy1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOndzOnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJtZXRhc3RhdHMtYXBpIiwibWV0aG9kcyI6WyJtZXRhc3RhdHMtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6InJpc2stbWFuYWdlbWVudC1hcGkiLCJtZXRob2RzIjpbInJpc2stbWFuYWdlbWVudC1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoiY29weWZhY3RvcnktYXBpIiwibWV0aG9kcyI6WyJjb3B5ZmFjdG9yeS1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoibXQtbWFuYWdlci1hcGkiLCJtZXRob2RzIjpbIm10LW1hbmFnZXItYXBpOnJlc3Q6ZGVhbGluZzoqOioiLCJtdC1tYW5hZ2VyLWFwaTpyZXN0OnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJiaWxsaW5nLWFwaSIsIm1ldGhvZHMiOlsiYmlsbGluZy1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfV0sImlnbm9yZVJhdGVMaW1pdHMiOmZhbHNlLCJ0b2tlbklkIjoiMjAyMTAyMTMiLCJpbXBlcnNvbmF0ZWQiOmZhbHNlLCJyZWFsVXNlcklkIjoiNWM2MTVhMDY0NTNiY2E0YTFhYTdkNGNlODc5MzI4NmYiLCJpYXQiOjE3NzkzMTI4ODJ9.VklJoE6hwcxOSUIINf-tVqOitXzZSf3AfTf2C5oZtnelZnH5F-a6rs1h9V7yNMdch7GVqaSh1reZMH7ZO1zlXOM-Pr2RGJTxs6_iZna3SLHMavyRbTf1Wh9wUu7z16rlsIenj4EVUE4Fuw_aDMpiGELwhPhOlEQa7GKiqCwOWcxox-eYaVYRltQZBiMPEsFD6Dx69SlCx3Ax9Ky73x-BhAcg5znbVM9A3ZT61TfOXZZDIi7tK1ImtWEhI1ZaellmvnZde45V11kikGbkBPSiu9U4Ag6im8CietZRLfxm2uDqzxdPJ1mgSwZvuPFRasHKwgKimp0gmasmIXu7XhOpTv65pCSg3n2v9BeM-WumMi4nJEtrhdxblrNswP4LrFSbuSDSKq11NUQJRXvtssN1i-Dd0RY-bxCZ-eMUvUbCNoyxr5vNmau2oeLSA3M-VGlN8NCF74qOcJKnEIXM_A5qi1NoSAj7emmIE8Oedj0nnK3tOrjs94Hn4N1EZfkNVTcmFVyUoLW9yQKd9sgfhidLOmZ9RaM0iG6G2WsK39v-2_nsj2NwhSQ_bYcMG9bC9tgBOiX8yVA9Y_X9-Yng2w0wI6Hy1wOJ8UTh6PdGAhki38RLulXS657XR3POnqT0jSAsSsisN9C5_hPE7lpY-NW7v5aBZn8pGNlB2PXNnkB885E"); const mtAccount = await metaApi.metatraderAccountApi.getAccount(metaApiAccountId); await mtAccount.deploy(); await mtAccount.waitConnected(); let connection = mtAccount.getRPCConnection(); await connection.connect(); await connection.waitSynchronized(); const suffix = settings.symbolSuffix ? settings.symbolSuffix.trim() : ''; const sym = pair.includes('/') ? (pair.replace('/', '') + suffix).toUpperCase() : (pair + suffix); const volume = parseFloat((settings.riskPerTrade ? settings.riskPerTrade / 100 : 0.01).toFixed(2)); let tradeResult; if (decision === 'BUY') { tradeResult = await connection.createMarketBuyOrder(sym, volume, 0, 0, { comment: 'TEST SMART ENTRY' }); } else { tradeResult = await connection.createMarketSellOrder(sym, volume, 0, 0, { comment: 'TEST SMART ENTRY' }); } res.json({ success: true, tradeResult }); } catch(e: any) { const errString = e.message || e.toString(); if (errString.toLowerCase().includes("permission") || errString.toLowerCase().includes("insufficient")) { return res.json({ error: "Permissões insuficientes na MetaAPI. Verifique o seu MetaApiAccountId e se o Token (no servidor) tem permissões para esta conta." }); } if (errString.toLowerCase().includes("not found")) { return res.json({ error: "Conta de Trading (Account ID) não encontrada na MetaAPI. Verifique se o Account ID está correto e se pertence à sua conta." }); } res.json({ error: errString, stack: e.stack }); } }); // Finnhub endpoint proxy mapped to Twelve format for compat app.get("/api/twelve/quote", async (req, res) => { const symbol = req.query.symbol || "EUR/USD"; try { if (symbol.toString().includes('Index') || symbol.toString().includes('BOOM') || symbol.toString().includes('CRASH')) { return res.json({ close: "15000" }); } let symbolYF = getYfSymbol(symbol.toString()); const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1d&range=1d`); const price = response.data.chart.result[0].meta.regularMarketPrice; res.json({ close: price.toString() }); } catch (error: any) { console.error(`Error fetching proxy quote for ${symbol}:`, error.message); // Fallback res.json({ close: "1.000" }); } }); // Proxy for history mapped to Twelve format app.get("/api/twelve/history", async (req, res) => { const symbol = req.query.symbol as string; let symbolYF = ""; try { if (symbol.toString().includes('Index') || symbol.toString().includes('BOOM') || symbol.toString().includes('CRASH')) { return res.json({ values: [] }); } symbolYF = getYfSymbol(symbol.toString()); const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=15m&range=5d`); const result = response.data.chart.result[0]; const quote = result.indicators.quote[0]; const timestamps = result.timestamp; if (timestamps && quote) { const values = []; for (let i = timestamps.length - 1; i >= 0; i--) { if (quote.close[i] !== null) { values.push({ datetime: new Date(timestamps[i] * 1000).toISOString(), open: quote.open[i].toString(), high: quote.high[i].toString(), low: quote.low[i].toString(), close: quote.close[i].toString(), volume: (quote.volume[i] || 0).toString() }); } } res.json({ values }); } else { res.json({ values: [] }); } } catch (error: any) { console.error(`Error fetching proxy History for ${symbol}:`, error.message); res.json({ values: [] }); } }); // OneSignal send notification endpoint app.post("/api/onesignal/notify", async (req, res) => { const { title, message } = req.body; const appId = process.env.ONESIGNAL_APP_ID; const restApiKey = process.env.ONESIGNAL_REST_API_KEY; if (!appId || !restApiKey) { return res.status(500).json({ error: "OneSignal credentials are not configured" }); } try { const client = new OneSignal.Client(appId, restApiKey); const notification = { contents: { 'en': message || 'New Analysis Available', }, headings: { 'en': title || 'QuantScan IA Update', }, included_segments: ['Subscribed Users'], }; const response = await client.createNotification(notification); res.json({ success: true, response }); } catch (error) { console.error("Error sending OneSignal notification", error); res.status(500).json({ error: "Failed to send notification" }); } }); // MetaAPI Provisioning Route app.post("/api/metaapi/provision", async (req, res) => { const { platform, login, password, serverName } = req.body; // User requested token hardcoded const token = process.env.METAAPI_TOKEN || "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI1YzYxNWEwNjQ1M2JjYTRhMWFhN2Q0Y2U4NzkzMjg2ZiIsImFjY2Vzc1J1bGVzIjpbeyJpZCI6InRyYWRpbmctYWNjb3VudC1tYW5hZ2VtZW50LWFwaSIsIm1ldGhvZHMiOlsidHJhZGluZy1hY2NvdW50LW1hbmFnZW1lbnQtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVzdC1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcnBjLWFwaSIsIm1ldGhvZHMiOlsibWV0YWFwaS1hcGk6d3M6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6Im1ldGFhcGktcmVhbC10aW1lLXN0cmVhbWluZy1hcGkiLCJtZXRob2RzIjpbIm1ldGFhcGktYXBpOndzOnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJtZXRhc3RhdHMtYXBpIiwibWV0aG9kcyI6WyJtZXRhc3RhdHMtYXBpOnJlc3Q6cHVibGljOio6KiJdLCJyb2xlcyI6WyJyZWFkZXIiLCJ3cml0ZXIiXSwicmVzb3VyY2VzIjpbIio6JFVTRVJfSUQkOioiXX0seyJpZCI6InJpc2stbWFuYWdlbWVudC1hcGkiLCJtZXRob2RzIjpbInJpc2stbWFuYWdlbWVudC1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoiY29weWZhY3RvcnktYXBpIiwibWV0aG9kcyI6WyJjb3B5ZmFjdG9yeS1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciIsIndyaXRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfSx7ImlkIjoibXQtbWFuYWdlci1hcGkiLCJtZXRob2RzIjpbIm10LW1hbmFnZXItYXBpOnJlc3Q6ZGVhbGluZzoqOioiLCJtdC1tYW5hZ2VyLWFwaTpyZXN0OnB1YmxpYzoqOioiXSwicm9sZXMiOlsicmVhZGVyIiwid3JpdGVyIl0sInJlc291cmNlcyI6WyIqOiRVU0VSX0lEJDoqIl19LHsiaWQiOiJiaWxsaW5nLWFwaSIsIm1ldGhvZHMiOlsiYmlsbGluZy1hcGk6cmVzdDpwdWJsaWM6KjoqIl0sInJvbGVzIjpbInJlYWRlciJdLCJyZXNvdXJjZXMiOlsiKjokVVNFUl9JRCQ6KiJdfV0sImlnbm9yZVJhdGVMaW1pdHMiOmZhbHNlLCJ0b2tlbklkIjoiMjAyMTAyMTMiLCJpbXBlcnNvbmF0ZWQiOmZhbHNlLCJyZWFsVXNlcklkIjoiNWM2MTVhMDY0NTNiY2E0YTFhYTdkNGNlODc5MzI4NmYiLCJpYXQiOjE3NzkzMTI4ODJ9.VklJoE6hwcxOSUIINf-tVqOitXzZSf3AfTf2C5oZtnelZnH5F-a6rs1h9V7yNMdch7GVqaSh1reZMH7ZO1zlXOM-Pr2RGJTxs6_iZna3SLHMavyRbTf1Wh9wUu7z16rlsIenj4EVUE4Fuw_aDMpiGELwhPhOlEQa7GKiqCwOWcxox-eYaVYRltQZBiMPEsFD6Dx69SlCx3Ax9Ky73x-BhAcg5znbVM9A3ZT61TfOXZZDIi7tK1ImtWEhI1ZaellmvnZde45V11kikGbkBPSiu9U4Ag6im8CietZRLfxm2uDqzxdPJ1mgSwZvuPFRasHKwgKimp0gmasmIXu7XhOpTv65pCSg3n2v9BeM-WumMi4nJEtrhdxblrNswP4LrFSbuSDSKq11NUQJRXvtssN1i-Dd0RY-bxCZ-eMUvUbCNoyxr5vNmau2oeLSA3M-VGlN8NCF74qOcJKnEIXM_A5qi1NoSAj7emmIE8Oedj0nnK3tOrjs94Hn4N1EZfkNVTcmFVyUoLW9yQKd9sgfhidLOmZ9RaM0iG6G2WsK39v-2_nsj2NwhSQ_bYcMG9bC9tgBOiX8yVA9Y_X9-Yng2w0wI6Hy1wOJ8UTh6PdGAhki38RLulXS657XR3POnqT0jSAsSsisN9C5_hPE7lpY-NW7v5aBZn8pGNlB2PXNnkB885E"; if (!platform || !login || !password || !serverName) { return res.status(400).json({ error: "Missing required MT4/MT5 fields" }); } try { // @ts-ignore const metaApi = new MetaApi(token); const accounts = await metaApi.metatraderAccountApi.getAccountsWithInfiniteScrollPagination(); const existingAccount = accounts.find((a: any) => a.login === login && a.server === serverName); if (existingAccount) { return res.json({ success: true, accountId: existingAccount.id, state: existingAccount.state }); } const account = await metaApi.metatraderAccountApi.createAccount({ name: `QuantScan User ${login}`, login: login, password: password, server: serverName, platform: platform, magic: 1000 }); res.json({ success: true, accountId: account.id, state: account.state }); } catch (error: any) { console.error("MetaAPI Connection Error:", error); res.status(500).json({ error: error.message || "Failed to connect to MetaAPI" }); } }); // Vite middleware for development if (process.env.NODE_ENV !== "production") { const vite = await createViteServer({ server: { middlewareMode: true, hmr: false }, appType: "spa", }); app.use(vite.middlewares); } else { const distPath = path.join(process.cwd(), 'dist'); app.use(express.static(distPath)); app.get('*', (req, res) => { res.sendFile(path.join(distPath, 'index.html')); }); } app.listen(PORT, "0.0.0.0", () => { console.log('Server running on http://localhost:' + PORT); }); } startServer();