2026-05-20 03:26:54 -07:00
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" ;
2026-05-25 05:52:17 -07:00
const MetaApi = typeof MetaApiPkg === "function" ? MetaApiPkg : ( MetaApiPkg as any ). default || MetaApiPkg ;
2026-05-20 03:26:54 -07:00
2026-05-29 11:22:12 -07:00
2026-05-20 04:12:56 -07:00
// Import Firebase (Server-side compatible since v9 supports Node environments)
import { db } from "./src/lib/firebase" ;
2026-05-25 05:52:17 -07:00
import { doc , getDoc , setDoc , addDoc , collection , runTransaction , query , where , getDocs , updateDoc , orderBy , limit } from "firebase/firestore" ;
2026-05-20 04:12:56 -07:00
2026-05-20 03:26:54 -07:00
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" });
});
2026-05-25 05:52:17 -07:00
// --- BACKGROUND WORKER NODE (CRON JOB) ---
2026-05-29 11:22:12 -07:00
function getYfSymbol ( pair : string ) : string {
if ( pair === 'XAU/USD' ) return 'GC=F' ;
if ( pair === 'US100' || pair === 'US100.std' ) return 'NQ=F' ;
if ( pair === 'US30' || pair === 'US30.std' ) return 'YM=F' ;
if ( pair === 'BTC/USD' || pair === 'BTCUSD' ) return 'BTC-USD' ;
if ( pair . includes ( 'BTC' )) return 'BTC-USD' ;
const cleanPair = pair . toString (). replace ( '/' , '' );
if ( cleanPair . endsWith ( 'USD' ) || cleanPair . endsWith ( 'JPY' ) || cleanPair . endsWith ( 'EUR' ) || cleanPair . endsWith ( 'GBP' ) || cleanPair . endsWith ( 'CAD' ) || cleanPair . endsWith ( 'CHF' ) || cleanPair . endsWith ( 'AUD' ) || cleanPair . endsWith ( 'NZD' )) {
return cleanPair + '=X' ;
}
return cleanPair + '=X' ;
}
const sendTelegramAlertServer = async ( text : string , botToken : string , chatId : string , replyToMessageId? : number ) => {
2026-05-25 05:52:17 -07:00
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 {
2026-05-29 11:22:12 -07:00
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 );
2026-05-25 05:52:17 -07:00
}
} catch ( e ) {
2026-05-29 11:22:12 -07:00
console . error ( "Yahoo Finance data fetch error:" , e );
2026-05-25 05:52:17 -07:00
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 } ` ;
}
2026-05-25 05:52:17 -07:00
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
2026-05-29 11:22:12 -07:00
const pairs = [ 'EUR/USD' , 'GBP/USD' , 'XAU/USD' , 'USD/JPY' , 'Boom 1000 Index' , 'Crash 1000 Index' , 'Boom 500 Index' , 'Crash 500 Index' , 'Step Index' , 'Volatility 75 Index' ];
2026-05-25 05:52:17 -07:00
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 ;
}
2026-05-29 11:22:12 -07:00
const apiKey = process . env . FINNHUB_API_KEY || process . env . TWELVE_DATA_API_KEY ;
2026-05-25 05:52:17 -07:00
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."
2026-05-25 05:52:17 -07:00
];
let context = SMC_Contexts [ Math . floor ( Math . random () * SMC_Contexts . length )];
2026-05-29 11:22:12 -07:00
let symbolYF = getYfSymbol ( randomPair );
try {
if ( randomPair . includes ( 'Index' )) {
currentPrice = 15000 ;
isBullish = Math . random () > 0.5 ;
} else {
const yfRes = await axios . get ( `https://query1.finance.yahoo.com/v8/finance/chart/ ${ symbolYF } ?interval=15m&range=1d` );
const result = yfRes . data . chart . result [ 0 ];
currentPrice = parseFloat ( result . meta . regularMarketPrice );
const closes = result . indicators . quote [ 0 ]. close || [];
const validCloses = closes . filter (( c : number ) => c !== null );
const len = validCloses . length ;
if ( len >= 3 ) {
const oldPrice = parseFloat ( validCloses [ len - 3 ]);
isBullish = currentPrice > oldPrice ;
context = isBullish ?
"SMC: Mitigação de Demand Block em zona de desconto (Discount). Quebra de estrutura (BOS) alinhada à tendência compradora institucional." :
"SMC: Captura de Buy Side Liquidity (BSL) no topo (Premium). Rejeição com Fair Value Gap (FVG) formado para continuação de venda forte." ;
}
2026-05-25 05:52:17 -07:00
}
2026-05-29 11:22:12 -07:00
} catch ( e : any ) {
console . log ( "[Worker] Could not fetch real price from Yahoo Finance API. Fallback to Mock or skip." , e . message );
2026-05-25 05:52:17 -07:00
}
// 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." );
2026-05-25 05:52:17 -07:00
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: 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
2026-05-29 11:22:12 -07:00
if ( userData . metaApiAccountId && settings . engineRunning && settings . smartEntry && settings . selectedAsset === randomPair ) {
2026-05-25 05:52:17 -07:00
try {
console . log ( `[AutoTrading] Executando ordem para o usuário ${ userData . email } em ${ randomPair } ` );
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 orderType = decision === 'BUY' ? 'ORDER_TYPE_BUY' : 'ORDER_TYPE_SELL' ;
2026-05-29 11:22:12 -07:00
const suffix = settings . symbolSuffix ? settings . symbolSuffix . trim () : '' ;
const sym = randomPair . includes ( '/' ) ? ( randomPair . replace ( '/' , '' ) + suffix ). toUpperCase () : ( randomPair + suffix );
2026-05-25 05:52:17 -07:00
const volume = ( settings . riskPerTrade ? settings . riskPerTrade / 100 : 0.01 ). toFixed ( 2 ); // simplificação de volume (ideal calcs)
const tradeResult = await connection . createMarketOrder ( sym , orderType , parseFloat ( 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 : parseFloat ( volume ),
price : currentPrice ,
stopLoss : parseFloat ( signalData . stopLoss ),
takeProfit : parseFloat ( signalData . takeProfit ),
timestamp : Date.now ()
});
} catch ( dbErr ) {
console . error ( "Erro salvando auto_trade no db:" , dbErr );
}
2026-05-25 05:52:17 -07:00
} catch ( userErr ) {
console . error ( `[AutoTrading] Erro executando ordem para usuário ${ userData . email } :` , userErr );
}
}
}
} catch ( apiErr ) {
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/admin/test-auto-trading" , async ( req , res ) => {
require ( 'fs' ). appendFileSync ( 'debug_log.txt' , 'HIT ROUTE\n' );
const { uid , pair , decision , price , metaApiAccountId , settings } = req . body ;
try {
if ( ! metaApiAccountId || ! settings || ! settings . engineRunning ) {
return res . json ({ error : "Auto trading engine not running or MetaApi not setup" });
}
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 orderType = decision === 'BUY' ? 'ORDER_TYPE_BUY' : 'ORDER_TYPE_SELL' ;
const suffix = settings . symbolSuffix ? settings . symbolSuffix . trim () : '' ;
const sym = pair . includes ( '/' ) ? ( pair . replace ( '/' , '' ) + suffix ). toUpperCase () : ( pair + suffix );
const volume = ( settings . riskPerTrade ? settings . riskPerTrade / 100 : 0.01 ). toFixed ( 2 );
const tradeResult = await connection . createMarketOrder ( sym , orderType , parseFloat ( volume ), 0 , 0 , { comment : 'TEST SMART ENTRY' });
res . json ({ success : true , tradeResult });
} catch ( e : any ) {
const errString = e . message || e . toString ();
require ( 'fs' ). appendFileSync ( 'debug_log.txt' , 'ERR: ' + errString + '\n' );
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." });
}
res . json ({ error : errString , stack : e.stack });
}
});
// Finnhub endpoint proxy mapped to Twelve format for compat
2026-05-20 03:26:54 -07:00
app . get ( "/api/twelve/quote" , async ( req , res ) => {
const symbol = req . query . symbol || "EUR/USD" ;
try {
2026-05-29 11:22:12 -07:00
if ( symbol . toString (). includes ( 'Index' )) {
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 () });
2026-05-20 03:26:54 -07:00
} catch ( error ) {
2026-05-29 11:22:12 -07:00
console . error ( "Error fetching proxy quote" , error );
2026-05-20 03:26:54 -07:00
res . status ( 500 ). json ({ error : "Failed to fetch market data" });
}
});
2026-05-29 11:22:12 -07:00
// Proxy for history mapped to Twelve format
2026-05-20 03:26:54 -07:00
app . get ( "/api/twelve/history" , async ( req , res ) => {
const symbol = req . query . symbol as string ;
try {
2026-05-29 11:22:12 -07:00
if ( symbol . toString (). includes ( 'Index' )) {
return res . status ( 500 ). json ({ error : "Not supported for index" });
}
let 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 : [] });
}
2026-05-20 03:26:54 -07:00
} catch ( error ) {
2026-05-29 11:22:12 -07:00
console . error ( "Error fetching proxy History" , error );
2026-05-20 03:26:54 -07:00
res . status ( 500 ). json ({ error : "Failed to fetch market history" });
}
});
// 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" });
}
});
2026-05-25 05:52:17 -07:00
// 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 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" });
}
});
2026-05-20 03:26:54 -07:00
// 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 },
2026-05-20 03:26:54 -07:00
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 ();