2d6e1a0809
Add Deriv API websocket client, route handling, and Firestore security rules to support automated trading integration.
1019 lines
52 KiB
TypeScript
1019 lines
52 KiB
TypeScript
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";
|
|
|
|
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) ---
|
|
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';
|
|
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$/, '');
|
|
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
|
|
}
|
|
|
|
if (process.env.TWELVE_DATA_API_KEY) {
|
|
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=${process.env.TWELVE_DATA_API_KEY}`);
|
|
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;
|
|
}
|
|
|
|
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')) {
|
|
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}`;
|
|
}
|
|
}
|
|
|
|
// 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<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;
|
|
}
|
|
} 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, "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);
|
|
|
|
try {
|
|
await addDoc(collection(db, "users", userSnap.id, "auto_trades"), {
|
|
pair: randomPair,
|
|
decision: decision,
|
|
volume: 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);
|
|
|
|
let volume = parseFloat((settings.riskPerTrade ? settings.riskPerTrade / 100 : 0.01).toFixed(2));
|
|
if (settings.lotType === 'fixed') volume = parseFloat(settings.fixedLot || 0.01);
|
|
|
|
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' });
|
|
}
|
|
|
|
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 });
|
|
} 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 {
|
|
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({ 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" });
|
|
}
|
|
});
|
|
|
|
// 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: [] });
|
|
}
|
|
|
|
if (process.env.TWELVE_DATA_API_KEY) {
|
|
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=${process.env.TWELVE_DATA_API_KEY}`);
|
|
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" }
|
|
});
|
|
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),
|
|
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" });
|
|
}
|
|
});
|
|
|
|
// 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();
|