Files
QuantScan-Pro-/server.ts
T

1032 lines
52 KiB
TypeScript
Raw Normal View History

import express from "express";
import path from "path";
import axios from "axios";
import { createServer as createViteServer } from "vite";
import * as OneSignal from 'onesignal-node';
2026-05-29 11:22:12 -07:00
import MetaApiPkg from "metaapi.cloud-sdk/dist/index";
const MetaApi = typeof MetaApiPkg === "function" ? MetaApiPkg : (MetaApiPkg as any).default || MetaApiPkg;
2026-05-29 11:22:12 -07:00
// 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";
import derivRoutes from "./src/routes/deriv.routes.ts";
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" });
});
// Mount Deriv Routes
app.use("/api/deriv", derivRoutes);
// --- BACKGROUND WORKER NODE (CRON JOB) ---
2026-05-29 11:22:12 -07:00
function getYfSymbol(pair: string): string {
if (pair.includes('XAU')) return 'GC=F';
if (pair.includes('XAG')) return 'SI=F';
if (pair.includes('US100')) return 'NQ=F';
if (pair.includes('US30')) return 'YM=F';
2026-05-29 11:22:12 -07:00
if (pair.includes('BTC')) return 'BTC-USD';
if (pair.includes('ETH')) return 'ETH-USD';
// Clean punctuation and common broker suffixes
const cleanPair = pair.toString().replace('/', '').replace(/\.M$/, '').replace(/\.std$/, '');
2026-05-29 11:22:12 -07:00
return cleanPair + '=X';
}
async function getCurrentPriceProxy(symbol: string): Promise<number | null> {
const symStr = symbol.trim().toUpperCase();
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
return null; // Synthetic indices handled separately or simulated
}
const apiKey = process.env.TWELVE_DATA_API_KEY || "e3143522be4c4aac89e1f9a39925ed80";
if (apiKey) {
try {
let twelveSymbol = symStr;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const res = await axios.get(`https://api.twelvedata.com/price?symbol=${twelveSymbol}&apikey=${apiKey}`);
if (res.data && res.data.price) return parseFloat(res.data.price);
} catch (e: any) {
// Fallback silently if TwelveData is rate-limited or fails
}
}
const isCrypto = symStr.includes('BTC') || symStr.includes('ETH') || symStr.includes('USDT') || symStr.includes('BNB');
if (isCrypto) {
try {
const binanceSymbol = symStr.replace(/[^A-Z0-9]/g, '');
const res = await axios.get(`https://api.binance.com/api/v3/ticker/price?symbol=${binanceSymbol}`);
if (res.data && res.data.price) return parseFloat(res.data.price);
} catch (e) {}
}
try {
let symbolYF = getYfSymbol(symStr);
const yfRes = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=1m&range=1d`, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
const result = yfRes.data.chart.result[0];
return parseFloat(result.meta.regularMarketPrice);
} catch(e: any) {
console.warn("Yahoo Finance error:", e.message);
}
return null;
}
2026-05-29 11:22:12 -07:00
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 {
const symStr = signal.pair.toUpperCase();
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
2026-05-29 11:22:12 -07:00
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 {
currentPrice = await getCurrentPriceProxy(symStr);
}
} catch(e: any) {
console.error("Proxy price 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}`;
}
}
2026-05-29 11:22:12 -07:00
// 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 = [
2026-05-29 11:22:12 -07:00
"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)];
2026-05-29 11:22:12 -07:00
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<any>(async (resolve, reject) => {
try {
const WebSocketModule = await import('ws');
const WebSocket = WebSocketModule.default || WebSocketModule;
const ws = new (WebSocket as any)('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);
} catch (e) {
reject(e);
}
});
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;
}
2026-05-29 11:22:12 -07:00
} 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!
2026-05-29 11:22:12 -07:00
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, "users", "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 {
// CHECK TRADING HOURS
if (settings.tradingHoursStart && settings.tradingHoursEnd) {
const tzNow = new Date();
const hc = tzNow.getUTCHours();
const mc = tzNow.getUTCMinutes();
const tc = hc * 60 + mc;
const [hs, ms] = settings.tradingHoursStart.split(':').map(Number);
const ts = hs * 60 + ms;
const [he, me] = settings.tradingHoursEnd.split(':').map(Number);
const te = he * 60 + me;
if (te < ts) { // crosses midnight
if (tc < ts && tc > te) continue; // skip trading
} else {
if (tc < ts || tc > te) continue; // skip trading
}
}
console.log(`[AutoTrading] Executando ordem para o usuário ${userData.email} em ${randomPair} via ${userData.mtPlatform}`);
let volume = parseFloat((settings.riskPerTrade ? settings.riskPerTrade / 100 : 0.01).toFixed(2));
if (settings.lotType === 'fixed') volume = parseFloat(settings.fixedLot || 0.01);
if (settings.breakEvenEnabled) console.log(`[AutoTrading] Break Even Track habilitado para ${userData.email}`);
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);
2026-05-29 11:22:12 -07:00
try {
await addDoc(collection(db, "users", userSnap.id, "auto_trades"), {
pair: randomPair,
decision: decision,
volume: volume,
2026-05-29 11:22:12 -07:00
price: currentPrice,
stopLoss: parseFloat(signalData.stopLoss),
takeProfit: parseFloat(signalData.takeProfit),
timestamp: Date.now(),
platform: userData.mtPlatform || 'mt5'
2026-05-29 11:22:12 -07:00
});
} 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() });
});
2026-05-29 11:22:12 -07:00
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 });
}
});
2026-05-29 11:22:12 -07:00
app.post("/api/admin/test-auto-trading", async (req, res) => {
const { uid, pair, decision, price, metaApiAccountId, settings, mtPlatform, mtLogin } = req.body;
2026-05-29 11:22:12 -07:00
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." });
2026-05-29 11:22:12 -07:00
}
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);
let volume = parseFloat((settings.riskPerTrade ? settings.riskPerTrade / 100 : 0.01).toFixed(2));
if (settings.lotType === 'fixed') volume = parseFloat(settings.fixedLot || 0.01);
2026-05-29 11:22:12 -07:00
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' });
}
2026-05-29 11:22:12 -07:00
let parsedResult: any = {};
if (tradeResult) {
try {
// Extract plain properties to avoid circular references from MetaApi objects
parsedResult = {
orderId: tradeResult.orderId,
positionId: tradeResult.positionId,
volume: tradeResult.volume,
price: tradeResult.price,
type: tradeResult.type,
comment: tradeResult.comment,
done: tradeResult.done,
// For Deriv API fallback
buy: tradeResult.buy?.transaction_id,
sell: tradeResult.sell?.transaction_id,
error: tradeResult.error ? String(tradeResult.error) : undefined
};
} catch(e) {
parsedResult = { message: "Trade executed, but could not parse result due to circular references." };
}
}
res.json({ success: true, tradeResult: parsedResult });
2026-05-29 11:22:12 -07:00
} 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." });
}
2026-05-29 11:22:12 -07:00
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 {
const symStr = symbol.toString().toUpperCase();
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
2026-05-29 11:22:12 -07:00
return res.json({ close: "15000" });
}
const price = await getCurrentPriceProxy(symbol.toString());
if (price !== null) {
res.json({ close: price.toString() });
} else {
res.json({ close: "1.000" });
}
} catch (error: any) {
console.error(`Error fetching proxy quote for ${symbol}:`, error.message);
res.json({ close: "1.000" });
}
});
2026-05-29 11:22:12 -07:00
// Proxy for history mapped to Twelve format
app.get("/api/twelve/history", async (req, res) => {
const symbol = req.query.symbol as string;
try {
const symStr = symbol.toString().toUpperCase();
if (symStr.includes('INDEX') || symStr.includes('BOOM') || symStr.includes('CRASH') || symStr.includes('VIX') || symStr.includes('VOLATILITY') || symStr.includes('STEP')) {
return res.json({ values: [] });
2026-05-29 11:22:12 -07:00
}
const apiKey = process.env.TWELVE_DATA_API_KEY || "e3143522be4c4aac89e1f9a39925ed80";
if (apiKey) {
try {
let twelveSymbol = symStr;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const tkres = await axios.get(`https://api.twelvedata.com/time_series?symbol=${twelveSymbol}&interval=15min&apikey=${apiKey}`);
if (tkres.data && tkres.data.values) {
return res.json({ values: tkres.data.values });
}
} catch(e) {}
}
const isCrypto = symStr.includes('BTC') || symStr.includes('ETH') || symStr.includes('USDT') || symStr.includes('BNB');
if (isCrypto) {
try {
const binanceSymbol = symStr.replace(/[^A-Z0-9]/g, '');
const bRes = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&limit=200`);
if (bRes.data && Array.isArray(bRes.data)) {
const values = bRes.data.map((c: any) => ({
datetime: new Date(c[0]).toISOString().replace('T', ' ').substring(0, 19),
open: c[1], high: c[2], low: c[3], close: c[4], volume: c[5]
})).reverse();
return res.json({ values });
}
} catch(e) {}
}
let symbolYF = getYfSymbol(symbol.toString());
const response = await axios.get(`https://query1.finance.yahoo.com/v8/finance/chart/${symbolYF}?interval=15m&range=5d`, {
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" }
});
2026-05-29 11:22:12 -07:00
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().replace('T', ' ').substring(0, 19),
2026-05-29 11:22:12 -07:00
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'],
};
let finalResponse = "ok";
try {
const response = await client.createNotification(notification);
finalResponse = (response as any).id || "ok";
} catch (e) {
// ignore or handle
}
res.json({ success: true, response: finalResponse });
} 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" });
}
});
// Proxy for Economic Calendar (using fair economy due to 12Data missing macro economic endpoints)
app.get("/api/economic_calendar", async (req, res) => {
try {
const calendarRes = await axios.get("https://nfs.faireconomy.media/ff_calendar_thisweek.json");
return res.json({ success: true, data: calendarRes.data });
} catch(e: any) {
console.error("Economic calendar error:", e.message);
return res.status(500).json({ success: false, error: "Failed to fetch calendar data" });
}
});
// Vite middleware for development
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
2026-05-29 11:22:12 -07:00
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();