feat(auto-trading): integrate MetaApi for automated trading

- Add metaapi.cloud-sdk for broker integration
- Implement AutoTradingView component
- Extend Signal interface with secondary take-profit levels
- Update UI to include auto-trading navigation tab
This commit is contained in:
desartstudio95
2026-05-25 05:52:17 -07:00
parent 546f2670a0
commit b4df5a2d17
20 changed files with 3291 additions and 279 deletions
+1
View File
@@ -0,0 +1 @@
TWELVE_DATA_API_KEY=ff37da4ef2594ea0ac800f34b9512533
+5
View File
@@ -124,6 +124,11 @@ service cloud.firestore {
match /settings/{settingId} {
allow read: if true;
allow update, create, delete: if isDbAdmin();
}
// --- Telegram Active Signals (Usado pelo background worker) ---
match /telegram_active_signals/{document=**} {
allow read, write: if true;
}
}
}
+2
View File
@@ -0,0 +1,2 @@
import MetaApi from 'metaapi.cloud-sdk';
console.log(typeof MetaApi);
+2390
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -27,6 +27,7 @@
"firebase": "^12.12.1",
"lightweight-charts": "^5.2.0",
"lucide-react": "^0.546.0",
"metaapi.cloud-sdk": "^29.3.3",
"motion": "^12.23.24",
"onesignal-node": "^3.4.0",
"react": "^19.0.0",
+355 -99
View File
@@ -3,10 +3,12 @@ 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";
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 } from "firebase/firestore";
import { doc, getDoc, setDoc, addDoc, collection, runTransaction, query, where, getDocs, updateDoc, orderBy, limit } from "firebase/firestore";
async function startServer() {
const app = express();
@@ -19,6 +21,327 @@ async function startServer() {
res.json({ status: "ok" });
});
// --- BACKGROUND WORKER NODE (CRON JOB) ---
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);
const apiKey = process.env.TWELVE_DATA_API_KEY;
if (!apiKey) return; // Need real prices to monitor properly
for (const docSnap of querySnapshot.docs) {
const signal = docSnap.data();
let currentPrice = null;
try {
// Format symbol for Twelve data (e.g. EUR/USD)
let symbol = signal.pair;
if (!symbol.includes('/')) {
if (symbol.length === 6) {
symbol = symbol.substring(0, 3) + '/' + symbol.substring(3); // EURUSD -> EUR/USD
}
}
const response = await axios.get(`https://api.twelvedata.com/quote?symbol=${symbol}&apikey=${apiKey}`);
if (response.data && response.data.close) {
currentPrice = parseFloat(response.data.close);
}
} catch(e) {
console.error("Twelve data fetch error:", e);
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}`;
}
}
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'];
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;
}
const apiKey = process.env.TWELVE_DATA_API_KEY;
let currentPrice = 0;
let isBullish = Math.random() > 0.5;
const SMC_Contexts = [
"Manipulação Institucional detectada em suporte de H4, Sweep de liquidez seguido de quebra de estrutura (BOS).",
"Mitigação de Order Block (OB) diário, com desequilíbrio (Imbalance) preenchido em 15m.",
"Captura de Liquidez do Asia Session (Asia High/Low sweep) e confirmação via fluxo de ordens (Order Flow)."
];
let context = SMC_Contexts[Math.floor(Math.random() * SMC_Contexts.length)];
if (apiKey) {
try {
// Keep the symbol with slash for twelve data
const sym = randomPair;
const tsRes = await axios.get(`https://api.twelvedata.com/time_series?symbol=${sym}&interval=15min&outputsize=3&apikey=${apiKey}`);
if (tsRes.data && tsRes.data.values && tsRes.data.values.length > 0) {
const values = tsRes.data.values;
currentPrice = parseFloat(values[0].close);
if (values.length >= 3) {
const oldPrice = parseFloat(values[2].close);
isBullish = currentPrice > oldPrice;
context = isBullish ?
"Mitigação de Demand Block em zona de desconto. Quebra de estrutura (BOS) alinhada à tendência compradora (Smart Money)." :
"Captura de liquidez (Sweep) no topo (Premium). Inversão com fair value gap preenchido para continuação de venda.";
}
} else {
const res = await axios.get(`https://api.twelvedata.com/quote?symbol=${sym}&apikey=${apiKey}`);
if (res.data && res.data.close) currentPrice = parseFloat(res.data.close);
}
} catch (e) {
console.log("[Worker] Could not fetch real price from Twelve Data API. Rate Limit or Network error.");
}
}
// Se não conseguiu preço real, aborta a geração do sinal ao invés de usar preço falso!
if (currentPrice === 0) {
console.log("[Worker] Preço real não obtido (API Limits?). 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: 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 && settings.smartEntry && settings.selectedAsset === randomPair) {
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';
const sym = randomPair.replace('/', '');
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);
} 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() });
});
// Twelve Data endpoint
app.get("/api/twelve/quote", async (req, res) => {
const symbol = req.query.symbol || "EUR/USD";
@@ -84,6 +407,37 @@ async function startServer() {
}
});
// 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" });
}
});
// Vite middleware for development
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
@@ -99,104 +453,6 @@ async function startServer() {
});
}
// --- BACKGROUND WORKER NODE (CRON JOB) ---
const sendTelegramAlertServer = async (signalData: any, botToken: string, chatId: string) => {
try {
const emojis = { BUY: '🟢', SELL: '🔴', WAIT: '⏳' };
const signalType = signalData.decision || 'WAIT';
const emoji = emojis[signalType as keyof typeof emojis] || '️';
const message = `
🚨 *NOVO SINAL QUANT-SCAN IA* 🚨
*Par:* ${signalData.pair}
*Timeframe:* ${signalData.timeframe || '15m'}
*Ação:* ${emoji} *${signalType}*
*Confiança:* ${signalData.score}%
*Entrada:* ${signalData.entry || 'N/A'}
*Take Profit 1:* ${signalData.takeProfit || 'N/A'}
*Take Profit 2:* ${signalData.takeProfit2 || 'N/A'}
*Take Profit 3:* ${signalData.takeProfit3 || 'N/A'}
*Stop Loss:* ${signalData.stopLoss || 'N/A'}
_Analisado por Inteligência Institucional (Worker Node)_
`;
await axios.post(`https://api.telegram.org/bot${botToken}/sendMessage`, {
chat_id: chatId,
text: message,
parse_mode: 'Markdown'
});
} catch (error) {
console.error('Error sending Telegram alert from server:', error);
}
};
const runAutoScannerJob = async () => {
try {
const settingsRef = doc(db, 'settings', 'app');
let botToken = '';
let chatId = '';
let shouldExecute = false;
await runTransaction(db, async (transaction) => {
const settingsSnap = await transaction.get(settingsRef);
if (!settingsSnap.exists()) return;
const data = settingsSnap.data();
if (!data.autoScannerActive) return;
const intervalMins = data.autoScannerInterval || 15;
const now = Date.now();
const lastScan = data.lastAutoScan || 0;
// Check if enough time has passed based on interval
if (now - lastScan >= intervalMins * 60 * 1000) {
// Atomic lock: update lastAutoScan inside transaction
transaction.update(settingsRef, { lastAutoScan: now });
shouldExecute = true;
botToken = data.telegramBotToken || '';
chatId = data.telegramChatId || '';
}
});
if (shouldExecute) {
console.log("[Worker] Executing Auto-Scanner...");
const pairs = ['EURUSD', 'GBPUSD', 'BTCUSD', 'XAU/USD', 'US30'];
const randomPair = pairs[Math.floor(Math.random() * pairs.length)];
const decision = Math.random() > 0.5 ? 'BUY' : 'SELL';
const score = Math.floor(Math.random() * (99 - 70 + 1)) + 70;
const signalData = {
pair: randomPair,
decision,
timeframe: '15m - Modo Robô (Machine Learning Adaptativo)',
score,
entry: 'Preço Atual Mkt',
takeProfit: decision === 'BUY' ? '+25 pips' : '-25 pips',
stopLoss: decision === 'BUY' ? '-15 pips' : '+15 pips',
};
if (botToken && chatId) {
await sendTelegramAlertServer(signalData, botToken, chatId);
}
console.log(`[Worker] Sinais gerados e enviados para ${randomPair}`);
}
} catch (e) {
console.error("[Worker] Erro no Auto-Scanner:", 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.listen(PORT, "0.0.0.0", () => {
console.log('Server running on http://localhost:' + PORT);
});
+3
View File
@@ -6,6 +6,7 @@
import React, { useState, useEffect } from 'react';
import { Navbar } from './components/Navbar';
import { AnalysisView } from './components/AnalysisView';
import { AutoTradingView } from './components/AutoTradingView';
import { SignalHistory } from './components/SignalHistory';
import { DashboardStats } from './components/DashboardStats';
import { PlansView } from './components/PlansView';
@@ -622,6 +623,7 @@ export default function App() {
<span className="block text-[11px] font-black uppercase tracking-widest text-zinc-500 mb-1">Olá Humano, Bem-Vindo</span>
<h2 className="text-2xl font-black italic uppercase tracking-tighter text-white">
{activeTab === 'scan' && 'Scanner de IA'}
{activeTab === 'autoTrade' && 'Auto Trading'}
{activeTab === 'history' && 'Histórico de Sinais'}
{activeTab === 'stats' && 'Performance & Dados'}
{activeTab === 'profile' && 'Perfil do Usuário'}
@@ -668,6 +670,7 @@ export default function App() {
transition={{ duration: 0.25, ease: 'easeOut' }}
>
{activeTab === 'scan' && <AnalysisView userData={userData} onGoToHistory={() => setActiveTab('history')} />}
{activeTab === 'autoTrade' && <AutoTradingView userData={userData} onUpdate={setUserData} onNavigate={setActiveTab} isAdmin={isAdmin} />}
{activeTab === 'history' && <SignalHistory />}
{activeTab === 'stats' && <DashboardStats />}
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
-58
View File
@@ -1,58 +0,0 @@
import React, { useState } from 'react';
import { collection, addDoc } from 'firebase/firestore';
import { db, auth } from '../lib/firebase';
import { ImageUploader } from './ImageUploader';
export const AddTestimonialForm: React.FC = () => {
const [text, setText] = useState('');
const [imageUrls, setImageUrls] = useState<string[]>([]);
const [submitting, setSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!auth.currentUser || !text.trim()) return;
setSubmitting(true);
try {
await addDoc(collection(db, 'testimonials'), {
userId: auth.currentUser.uid,
userName: auth.currentUser.email || 'Usuário',
text: text,
timestamp: Date.now(),
imageUrls: imageUrls
});
setText('');
setImageUrls([]);
alert('Resultado enviado com sucesso!');
} catch (error) {
console.error(error);
alert('Erro ao enviar resultado.');
} finally {
setSubmitting(false);
}
};
if (!auth.currentUser) return null;
return (
<form onSubmit={handleSubmit} className="glass-card p-6 space-y-4 mt-8 max-w-2xl mx-auto">
<h3 className="font-black uppercase tracking-widest text-brand-red">Deixe o seu resultado</h3>
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
className="w-full bg-white/5 border border-white/10 rounded-lg p-3 text-white focus:outline-none focus:ring-1 focus:ring-brand-red"
placeholder="O que achou do QuantScanner?"
maxLength={500}
/>
<ImageUploader onUpload={setImageUrls} />
{imageUrls.length > 0 && <p className="text-xs text-green-500">{imageUrls.length} imagem(ns) carregada(s) com sucesso!</p>}
<button
disabled={submitting}
type="submit"
className="bg-brand-red text-white py-2 px-4 rounded-lg font-bold hover:bg-opacity-90 disabled:opacity-50"
>
{submitting ? 'Enviando...' : 'Enviar Resultado'}
</button>
</form>
)
}
+5 -2
View File
@@ -600,11 +600,14 @@ export const AdminDashboard: React.FC = () => {
</h4>
<ul className="ml-8 list-disc list-inside space-y-2 text-zinc-400">
<li><strong>Title:</strong> QuantScan IA Auto-Worker (ou qualquer nome)</li>
<li><strong>URL:</strong> Copie e cole a URL abaixo:</li>
<li><strong>URL:</strong> Copie a URL abaixo:</li>
</ul>
<div className="ml-8 bg-black border border-white/10 p-3 rounded-lg text-xs font-mono select-all overflow-x-auto text-green-400">
{window.location.origin}/api/cron/auto-scanner
{window.location.origin.replace('ais-dev', 'ais-pre')}/api/cron/auto-scanner
</div>
<p className="ml-8 text-[10px] text-brand-red font-bold">
Atenção: A URL deve usar "ais-pre" (sua versão pública/compartilhada). Se você usar a "ais-dev", o acesso será negado por conta do login de desenvolvedor. Você também precisa publicar/compartilhar o app pelo menos uma vez.
</p>
<h4 className="font-bold text-white flex items-center gap-2">
<span className="bg-blue-500/20 text-blue-400 w-6 h-6 rounded-full flex items-center justify-center text-xs border border-blue-500/30">3</span>
-1
View File
@@ -241,7 +241,6 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
setPreview(null);
setResult(null);
setError(null);
setMockPrice(null);
setAlerts([]);
};
+453
View File
@@ -0,0 +1,453 @@
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import {
Bot, Power, Activity, Settings2, Link, Zap, Shield,
BarChart2, TrendingUp, DollarSign, Send, Globe2, AlertTriangle,
Fingerprint, ChevronRight, Server, Cpu, Radio, Network, CheckCircle2
} from 'lucide-react';
import { cn } from '../lib/utils';
export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) => void, onNavigate?: (tab: string) => void, isAdmin?: boolean }> = ({ userData, onUpdate, onNavigate, isAdmin }) => {
const [robotActive, setRobotActive] = useState(false);
const [mtPlatform, setMtPlatform] = useState(userData?.mtPlatform || 'mt5');
const [mtLogin, setMtLogin] = useState(userData?.mtLogin || '');
const [mtPassword, setMtPassword] = useState(userData?.mtPassword || '');
const [mtServer, setMtServer] = useState(userData?.mtServer || '');
const [isSaving, setIsSaving] = useState(false);
const [saveSuccess, setSaveSuccess] = useState(false);
// Bot Settings State
const [tradingSettings, setTradingSettings] = useState([
{ key: 'smartEntry', label: "Smart Entry AI", desc: "Usa rede neural para entradas precisas", active: userData?.tradingSettings?.smartEntry ?? true },
{ key: 'copyTrades', label: "Copy Institutional Trades", desc: "Copia movimentos de Baleias e Bancos", active: userData?.tradingSettings?.copyTrades ?? true },
{ key: 'newsFilter', label: "News Filter (Alta Volatilidade)", desc: "Pausa o robô durante notícias vermelhas", active: userData?.tradingSettings?.newsFilter ?? false },
{ key: 'autoTp', label: "Auto Take Profit (Dinâmico)", desc: "Ajusta alvo baseado em liquidez", active: userData?.tradingSettings?.autoTp ?? true },
{ key: 'autoSl', label: "Auto Stop Loss (Trailing)", desc: "Acompanha o preço para garantir lucro", active: userData?.tradingSettings?.autoSl ?? true },
]);
const [riskPerTrade, setRiskPerTrade] = useState(userData?.tradingSettings?.riskPerTrade?.toString() || "1.5");
const [maxPositions, setMaxPositions] = useState(userData?.tradingSettings?.maxPositions?.toString() || "3");
const [selectedAsset, setSelectedAsset] = useState(userData?.tradingSettings?.selectedAsset || 'XAU/USD');
const saveSettingsToFirebase = async (newSettings: any[], newRisk: string, newMax: string, newAsset?: string) => {
if (!userData?.uid) return;
const { doc, updateDoc } = await import('firebase/firestore');
const { db } = await import('../lib/firebase');
const userRef = doc(db, 'users', userData.uid);
const assetToSave = newAsset ?? selectedAsset;
const updatedSettingsObj = {
smartEntry: newSettings[0].active,
copyTrades: newSettings[1].active,
newsFilter: newSettings[2].active,
autoTp: newSettings[3].active,
autoSl: newSettings[4].active,
riskPerTrade: parseFloat(newRisk) || 1.5,
maxPositions: parseInt(newMax) || 3,
selectedAsset: assetToSave
};
try {
await updateDoc(userRef, { tradingSettings: updatedSettingsObj });
if (onUpdate) {
onUpdate({ ...userData, tradingSettings: updatedSettingsObj });
}
} catch (e) {
console.error("Error saving settings:", e);
}
};
const handleToggleSetting = (index: number) => {
const newSettings = [...tradingSettings];
newSettings[index].active = !newSettings[index].active;
setTradingSettings(newSettings);
saveSettingsToFirebase(newSettings, riskPerTrade, maxPositions);
};
const handleRiskBlur = () => {
saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions);
};
const handleMaxBlur = () => {
saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions);
};
const handleAssetBlur = () => {
saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions, selectedAsset);
};
const handleDisconnectBroker = async () => {
if (!userData?.uid) return;
setIsSaving(true);
try {
const { doc, updateDoc } = await import('firebase/firestore');
const { db } = await import('../lib/firebase');
const userRef = doc(db, 'users', userData.uid);
await updateDoc(userRef, {
mtPlatform: '',
mtLogin: '',
mtPassword: '',
mtServer: '',
metaApiAccountId: null,
metaApiState: null
});
if (onUpdate) {
onUpdate({ ...userData, mtPlatform: '', mtLogin: '', mtPassword: '', mtServer: '', metaApiAccountId: null, metaApiState: null });
}
setMtPlatform('mt5');
setMtLogin('');
setMtPassword('');
setMtServer('');
} catch (e: any) {
console.error("Error disconnecting broker:", e);
alert(e.message || "Failed to disconnect");
} finally {
setIsSaving(false);
}
};
const handleConnectBroker = async () => {
if (!userData?.uid) return;
setIsSaving(true);
try {
// First try to provision through our backend proxy
const res = await fetch('/api/metaapi/provision', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
platform: mtPlatform,
login: mtLogin,
password: mtPassword,
serverName: mtServer
})
});
const data = await res.json();
if (!res.ok) {
throw new Error(data.error || 'Failed to connect to MetaAPI');
}
// Save to Firebase
const { doc, updateDoc } = await import('firebase/firestore');
const { db } = await import('../lib/firebase');
const userRef = doc(db, 'users', userData.uid);
await updateDoc(userRef, {
mtPlatform,
mtLogin,
mtPassword,
mtServer,
metaApiAccountId: data.accountId,
metaApiState: data.state
});
if (onUpdate) {
onUpdate({ ...userData, mtPlatform, mtLogin, mtPassword, mtServer, metaApiAccountId: data.accountId, metaApiState: data.state });
}
setSaveSuccess(true);
setTimeout(() => setSaveSuccess(false), 3000);
} catch (e: any) {
console.error("Error saving Broker config:", e);
alert(e.message || "Failed to connect to Terminal");
} finally {
setIsSaving(false);
}
};
const marketData = [
{ pair: 'EUR/USD', signal: 'BUY', score: 98, momentum: 'High', liquidity: 'Optimal', trend: 'Bullish' },
{ pair: 'GBP/USD', signal: 'SELL', score: 92, momentum: 'Medium', liquidity: 'High', trend: 'Bearish' },
{ pair: 'XAU/USD', signal: 'BUY', score: 99, momentum: 'Extreme', liquidity: 'Optimal', trend: 'Bullish' },
{ pair: 'BTC/USD', signal: 'SELL', score: 85, momentum: 'Low', liquidity: 'Medium', trend: 'Ranging' },
];
return (
<div className="max-w-7xl mx-auto p-4 md:p-8 space-y-8 animate-in fade-in duration-500">
{/* HEADER PRINCIPAL */}
<div className="flex flex-col justify-end md:flex-row md:items-end md:justify-between gap-6 relative p-8 min-h-[400px] rounded-2xl bg-black/40 border border-brand-red/20 overflow-hidden group">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-brand-red/10 via-transparent to-transparent opacity-50 pointer-events-none" />
<div className="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent pointer-events-none z-0" />
<img src="https://i.ibb.co/VcJRM0zZ/90a0c129-b771-41d6-ad30-634d1d2546c4.png" alt="Background" className="absolute inset-0 w-full h-full object-cover opacity-100 mix-blend-overlay pointer-events-none" />
<div className="flex items-center gap-6 relative z-10">
<div className="space-y-1">
<h1 className="text-3xl font-black italic uppercase tracking-tighter text-white drop-shadow-[0_0_10px_rgba(255,255,255,0.2)] flex items-center gap-3">
QUANTSCAN IA
</h1>
</div>
</div>
<div className="flex items-center gap-4 relative z-10">
<div className="text-right hidden md:block">
<div className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest mb-1">Status Global</div>
<div className={cn(
"text-xl font-black tracking-widest",
robotActive ? "text-brand-red drop-shadow-[0_0_10px_rgba(255,0,0,0.8)]" : "text-zinc-600"
)}>
{robotActive ? "ONLINE" : "OFFLINE"}
</div>
</div>
</div>
</div>
{/* DASHBOARD GRID */}
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* LEFT COLUMN: STATUS & BUTTONS */}
<div className="lg:col-span-1 space-y-6">
{/* STATUS CARDS REMOVED */}
{/* MAIN BUTTONS */}
<div className="space-y-3 bg-black/40 border border-white/10 p-4 rounded-2xl">
<button
onClick={() => setRobotActive(!robotActive)}
className={cn(
"relative w-full py-4 rounded-xl font-black italic uppercase tracking-widest flex items-center justify-center gap-3 transition-all duration-300 overflow-hidden group",
robotActive
? "bg-black text-brand-red border border-brand-red/50 shadow-[0_0_20px_rgba(255,0,0,0.3)] hover:scale-[0.98]"
: "bg-[#111] text-white border border-brand-red/40 shadow-[0_0_30px_rgba(255,0,0,0.15)] hover:border-brand-red hover:shadow-[0_0_40px_rgba(255,0,0,0.3)] hover:bg-[#1a0505] hover:scale-[1.02]"
)}>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-brand-red/20 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-500 pointer-events-none" />
<div className="absolute top-0 right-0 w-8 h-[2px] bg-brand-red shadow-[0_0_10px_rgba(255,0,0,1)]" />
<div className="absolute bottom-0 left-0 w-8 h-[2px] bg-brand-red shadow-[0_0_10px_rgba(255,0,0,1)]" />
<Power size={20} className={cn("relative z-10 transition-transform duration-300", !robotActive && "text-brand-red animate-pulse group-hover:scale-110")} />
<span className="relative z-10 tracking-[0.2em]">{robotActive ? "Halt System" : "Initialize Engine"}</span>
</button>
<div className="space-y-4 bg-black/40 border border-brand-red/20 p-5 rounded-2xl relative overflow-hidden">
<div className="absolute top-0 right-0 p-4 opacity-10">
<Cpu size={100} className="text-brand-red" />
</div>
<h3 className="font-black italic uppercase text-white tracking-widest text-sm flex items-center gap-2 relative z-10">
<Bot size={16} className="text-brand-red" /> Robôs Conectados
</h3>
<div className="space-y-3 relative z-10">
<div className="bg-black/50 border border-white/10 rounded-lg p-3 flex items-center justify-between">
<div>
<div className="text-xs font-bold text-white uppercase flex items-center gap-2">QuantScan IA <span className="bg-brand-red/20 text-brand-red text-[8px] px-2 py-0.5 rounded-full">PRO</span></div>
<div className="text-[10px] text-zinc-500 mt-0.5">Trading Institucional HFR</div>
</div>
<div className="w-2 h-2 rounded-full bg-green-500 shadow-[0_0_8px_rgba(0,255,0,0.8)]" />
</div>
</div>
</div>
<button
onClick={() => document.getElementById("broker-connection")?.scrollIntoView({ behavior: "smooth" })}
className="w-full py-3 rounded-xl font-bold uppercase tracking-wider text-xs flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white border border-white/10 transition-colors">
<Link size={16} /> Connect Broker
</button>
<button
onClick={() => onNavigate && onNavigate('history')}
className="w-full py-3 rounded-xl font-bold uppercase tracking-wider text-xs flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white border border-white/10 transition-colors">
<BarChart2 size={16} /> View Signals
</button>
<button
onClick={() => onNavigate && onNavigate('scan')}
className="w-full py-3 rounded-xl font-bold uppercase tracking-wider text-xs flex items-center justify-center gap-2 bg-white/5 hover:bg-white/10 text-white border border-white/10 transition-colors">
<Zap size={16} /> Auto Scanner
</button>
{isAdmin && (
<button
onClick={() => onNavigate && onNavigate('admin')}
className="w-full py-3 rounded-xl font-bold uppercase tracking-wider text-xs flex items-center justify-center gap-2 bg-[#0088cc]/20 hover:bg-[#0088cc]/30 text-[#0088cc] border border-[#0088cc]/30 transition-colors">
<Send size={16} /> Telegram Alerts
</button>
)}
</div>
</div>
{/* MIDDLE COLUMN: SETTINGS */}
<div className="lg:col-span-1 space-y-6">
<div className="bg-black/40 border border-white/10 p-5 rounded-2xl h-full">
<h3 className="font-black italic uppercase text-white tracking-widest text-sm mb-6 flex items-center gap-2 border-b border-white/5 pb-4">
<Settings2 size={16} className="text-brand-red" /> Auto Trading Settings
</h3>
<div className="space-y-6">
{tradingSettings.map((setting, i) => (
<div key={i} className="flex items-center justify-between gap-4">
<div className="flex flex-col">
<span className="text-xs font-bold text-white uppercase tracking-wider">{setting.label}</span>
<span className="text-[9px] text-zinc-500 mt-0.5">{setting.desc}</span>
</div>
<button
onClick={() => handleToggleSetting(i)}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none",
setting.active ? "bg-brand-red" : "bg-zinc-800"
)}
>
<span
className={cn(
"inline-block h-3 w-3 transform rounded-full bg-white transition-transform",
setting.active ? "translate-x-5" : "translate-x-1"
)}
/>
</button>
</div>
))}
<div className="pt-4 border-t border-white/5 space-y-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Ativo a Negociar</label>
<input
type="text"
placeholder="Ex: EUR/USD, XAU/USD..."
value={selectedAsset}
onChange={(e) => setSelectedAsset(e.target.value)}
onBlur={handleAssetBlur}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Risk per Trade (%)</label>
<input
type="number"
value={riskPerTrade}
onChange={(e) => setRiskPerTrade(e.target.value)}
onBlur={handleRiskBlur}
step="0.1"
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Max Open Positions</label>
<input
type="number"
value={maxPositions}
onChange={(e) => setMaxPositions(e.target.value)}
onBlur={handleMaxBlur}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
</div>
</div>
</div>
</div>
{/* RIGHT COLUMN: BROKERS & LIVE MARKET */}
<div className="lg:col-span-2 space-y-6">
{/* BROKER INTEGRATION (MT4/MT5) */}
<div id="broker-connection" className="bg-black/40 border border-white/10 p-5 rounded-2xl">
<h3 className="font-black italic uppercase text-white tracking-widest text-sm mb-6 flex items-center gap-2">
<Globe2 size={16} className="text-brand-red" /> MT4 / MT5 Server Connection
</h3>
<div className="space-y-4">
{userData?.metaApiAccountId ? (
<div className="bg-green-500/10 border border-green-500/30 rounded-xl p-6 text-center space-y-4">
<div className="w-16 h-16 bg-green-500/20 rounded-full flex items-center justify-center mx-auto mb-2">
<CheckCircle2 size={32} className="text-green-500" />
</div>
<h4 className="text-green-500 font-bold uppercase tracking-widest text-lg">Terminal Conectado</h4>
<p className="text-sm text-zinc-400">Conta: <span className="text-white font-mono">{mtLogin}</span> ({mtPlatform.toUpperCase()})</p>
<button
onClick={handleDisconnectBroker}
disabled={isSaving}
className="w-full mt-4 bg-zinc-900 border border-brand-red/30 hover:bg-brand-red/10 text-brand-red font-bold uppercase tracking-widest text-xs py-3 rounded-xl transition-colors disabled:opacity-50"
>
{isSaving ? "Desconectando..." : "Desconectar Terminal"}
</button>
</div>
) : (
<>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Platform</label>
<div className="relative">
<select
value={mtPlatform}
onChange={(e) => setMtPlatform(e.target.value)}
className="w-full bg-black border border-white/10 rounded-lg p-3 pr-8 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors appearance-none"
>
<option value="mt5">MetaTrader 5</option>
<option value="mt4">MetaTrader 4</option>
</select>
<ChevronRight className="absolute right-3 top-1/2 -translate-y-1/2 text-zinc-500 pointer-events-none rotate-90" size={14} />
</div>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Login (Account ID)</label>
<input
type="number"
placeholder="Ex: 10029384"
value={mtLogin}
onChange={(e) => setMtLogin(e.target.value)}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Password</label>
<input
type="password"
placeholder="••••••••"
value={mtPassword}
onChange={(e) => setMtPassword(e.target.value)}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Server</label>
<input
type="text"
placeholder="Ex: Deriv-Server-01"
value={mtServer}
onChange={(e) => setMtServer(e.target.value)}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
</div>
<div className="space-y-1 mt-2">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Trade Comment (Metatrader/Corretora)</label>
<input
type="text"
value="QuantScan IA"
disabled
className="w-full bg-black border border-white/10 rounded-lg py-3 px-4 text-green-500 font-bold text-sm focus:outline-none opacity-80"
/>
<p className="text-[10px] text-zinc-500 ml-1">Este comentário será enviado obrigatoriamente a cada trade executado, provando a autenticidade das operações na sua corretora.</p>
</div>
<button
onClick={handleConnectBroker}
disabled={isSaving || !mtLogin || !mtPassword || !mtServer}
className="w-full bg-brand-red/10 text-brand-red border border-brand-red/30 hover:bg-brand-red/20 font-bold uppercase tracking-widest text-xs py-3 rounded-xl transition-colors disabled:opacity-50"
>
{isSaving ? "Connecting..." : saveSuccess ? "Connected Successfully" : "Connect Terminal"}
</button>
</>
)}
</div>
</div>
{/* TELEGRAM SETTINGS WIDGET */}
<div className="bg-[#0088cc]/10 border border-[#0088cc]/30 p-5 rounded-2xl flex flex-col md:flex-row gap-6 items-center">
<div className="w-12 h-12 rounded-full bg-[#0088cc]/20 flex items-center justify-center shrink-0">
<Send size={24} className="text-[#0088cc]" />
</div>
<div className="flex-1 space-y-1 text-center md:text-left">
<h4 className="font-bold text-white uppercase text-sm">Alertas no Telegram</h4>
<p className="text-xs text-zinc-400">Receba notificações de entradas e saídas do robô instantaneamente no seu celular.</p>
</div>
<button className="bg-[#0088cc] hover:bg-[#0088cc]/80 text-white font-bold uppercase tracking-widest text-[10px] px-6 py-3 rounded-xl transition-colors shrink-0">
Configurar
</button>
</div>
</div>
</div>
</div>
);
};
-2
View File
@@ -7,7 +7,6 @@ import { MarketTicker } from './MarketTicker';
import Autoplay from 'embla-carousel-autoplay';
import { Carousel, CarouselContent, CarouselItem, CarouselNext, CarouselPrevious } from "./ui/carousel";
import { Card, CardContent } from "./ui/card";
import { AddTestimonialForm } from './AddTestimonialForm';
interface LandingPageProps {
onGetStarted: () => void;
@@ -336,7 +335,6 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<p className="text-zinc-500 text-sm text-center">Nenhum resultado postado ainda.</p>
)}
</div>
<AddTestimonialForm />
</div>
</section>
+2 -1
View File
@@ -1,5 +1,5 @@
import React from 'react';
import { LayoutDashboard, History, PieChart, Info, LogOut, CreditCard, ShieldCheck, User } from 'lucide-react';
import { LayoutDashboard, History, PieChart, Info, LogOut, CreditCard, ShieldCheck, User, Cpu } from 'lucide-react';
import { cn } from '../lib/utils';
interface NavbarProps {
@@ -12,6 +12,7 @@ interface NavbarProps {
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin, user }) => {
const mainTabs = [
{ id: 'scan', label: 'Scan IA', icon: LayoutDashboard },
{ id: 'autoTrade', label: 'Auto Trading', icon: Cpu },
{ id: 'history', label: 'Histórico', icon: History },
{ id: 'stats', label: 'Estatísticas', icon: PieChart },
];
-84
View File
@@ -191,90 +191,6 @@ export const ProfileView: React.FC<{
className="w-5 h-5 accent-brand-red rounded bg-brand-dark/50 border border-white/10 cursor-pointer"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Risco Por Trade (%)</label>
<input
type="number"
value={riskPerTrade}
onChange={(e) => setRiskPerTrade(e.target.value)}
placeholder="Ex: 2"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Máximo de Posições Simultâneas / Por Sinal</label>
<input
type="number"
value={maxPositionsPerSignal}
onChange={(e) => setMaxPositionsPerSignal(e.target.value)}
placeholder="Ex: 1"
min="1"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
<p className="text-[10px] text-zinc-500 ml-1">Recomendado: Para a segurança da banca, o cliente deve escolher quantas posições no máximo o IA pode abrir.</p>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Corretora (Broker)</label>
<select
value={broker}
onChange={(e) => setBroker(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
>
<option value="binance">Binance</option>
<option value="deriv">Deriv</option>
<option value="exness">Exness</option>
<option value="just_market">Just Market</option>
<option value="fxpro">FXPRO</option>
<option value="xm_global">XM GLOBAL</option>
<option value="custom">Outra (Custom/Webhook)</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">ID da Conta (Account ID)</label>
<input
type="text"
value={brokerAccountId}
onChange={(e) => setBrokerAccountId(e.target.value)}
placeholder="Ex: 12345678"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">API Key / Webhook URL (Apenas Leitura & Trade)</label>
<input
type="text"
value={brokerApiKey}
onChange={(e) => setBrokerApiKey(e.target.value)}
placeholder="Cole aqui a API Key / URL"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">API Secret / Server Token</label>
<input
type="password"
value={brokerApiSecret}
onChange={(e) => setBrokerApiSecret(e.target.value)}
placeholder="Cole aqui o seu Secret/Token"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Trade Comment (Metatrader/Corretora)</label>
<input
type="text"
value="QuantScan IA"
disabled
className="w-full bg-brand-dark/30 border border-white/5 rounded-xl py-3 px-4 text-green-500 font-bold text-sm focus:outline-none opacity-80"
/>
<p className="text-[10px] text-zinc-500 ml-1">Este comentário será enviado obrigatoriamente a cada trade executado, provando a autenticidade das operações na sua corretora.</p>
</div>
</>
) : (
<div className="p-4 bg-brand-red/10 border border-brand-red/20 rounded-xl space-y-2">
+2 -2
View File
@@ -24,7 +24,7 @@ export const SignalHistory: React.FC = () => {
const pendingSignals = signals.filter(s => s.result === SignalResult.PENDING);
if (pendingSignals.length === 0) return;
const pairs = Array.from(new Set(pendingSignals.map(s => s.pair)));
const pairs = Array.from(new Set<string>(pendingSignals.map(s => s.pair || '')));
try {
const promises = pairs.map(symbol => fetchCurrentPrice(symbol));
@@ -33,7 +33,7 @@ export const SignalHistory: React.FC = () => {
const newPrices: Record<string, number> = {};
results.forEach((price, index) => {
if (price !== null) {
newPrices[pairs[index]] = price;
newPrices[pairs[index] as string] = price;
}
});
setMarketPrices(prev => ({ ...prev, ...newPrices }));
+2
View File
@@ -23,6 +23,8 @@ export interface Signal {
entry: string;
stopLoss: string;
takeProfit: string;
takeProfit2?: string;
takeProfit3?: string;
score: number;
justification: string;
result: SignalResult;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+34 -30
View File
@@ -1,36 +1,40 @@
import { readFileSync } from 'fs';
import { initializeTestEnvironment, assertFails, assertSucceeds } from '@firebase/rules-unit-testing';
import { getFirestore, collection, query, where, getDocs, addDoc, updateDoc } from "firebase/firestore";
import { initializeApp } from "firebase/app";
async function main() {
const projectId = `test-project-${Date.now()}`;
const testEnv = await initializeTestEnvironment({
projectId,
firestore: {
rules: readFileSync('firestore.rules', 'utf8'),
},
});
const firebaseConfig = {
apiKey: "AIzaSyDVUxpAmWf57dGrj0dltwg-2d3jytGpJCY",
authDomain: "quantscan-pro.firebaseapp.com",
projectId: "quantscan-pro",
storageBucket: "quantscan-pro.firebasestorage.app",
messagingSenderId: "580184623755",
appId: "1:580184623755:web:4c5fcf6fd4a8f7bc13628e",
measurementId: "G-4WZHMKD7Y6"
};
const alice = testEnv.authenticatedContext('alice', { email: 'alice@example.com' });
const db = alice.firestore();
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
// Test signals query
const signalsRef = db.collection('signals');
const q = signalsRef.where('userId', '==', 'alice');
async function test() {
try {
await assertSucceeds(q.get());
console.log("SUCCESS: signals query passed");
} catch (err) {
console.error("FAILED: signals query failed", err);
}
// Next, let's try users list
try {
await assertFails(db.collection('users').get());
console.log("SUCCESS: non-admin users list correctly denied");
} catch(err) {
console.error("FAILED: non-admin users list passed or threw error we didn't expect", err);
const signalsRef = collection(db, "telegram_active_signals");
console.log("Adding doc...");
const ref = await addDoc(signalsRef, { status: "ACTIVE", pair: "TEST/USD" });
console.log("Added doc", ref.id);
console.log("Querying...");
const q = query(signalsRef, where("status", "==", "ACTIVE"));
const querySnapshot = await getDocs(q);
console.log("Query returned", querySnapshot.size);
for (const docSnap of querySnapshot.docs) {
if (docSnap.id === ref.id) {
console.log("Updating doc...");
await updateDoc(docSnap.ref, { status: "CLOSED" });
console.log("Update success!");
}
}
} catch (e) {
console.error("Error monitoring signals:", e);
}
}
main().then(() => process.exit(0)).catch(err => { console.error(err); process.exit(1); });
test();
+20
View File
@@ -0,0 +1,20 @@
import { createRequire } from "module";
const require = createRequire(import.meta.url);
const metaApiPkg = require("metaapi.cloud-sdk");
const MetaApi = typeof metaApiPkg === "function" ? metaApiPkg : metaApiPkg.default || metaApiPkg;
async function test() {
try {
const api = new MetaApi('fake_token_123');
await api.metatraderAccountApi.createAccount({
name: 'Test',
login: '123123',
password: 'password',
server: 'Server-1',
platform: 'mt4',
magic: 1000
});
} catch (err) {
console.log("Error details:", err.message, err.response?.data || err.details || err);
}
}
test();
+15
View File
@@ -0,0 +1,15 @@
const axios = require('axios');
async function test() {
try {
const res = await axios.post('http://localhost:3000/api/metaapi/provision', {
platform: 'mt4',
login: '123456',
password: 'mypassword',
serverName: 'Broker-Server'
});
console.log("SUCCESS:", res.data);
} catch (e) {
console.log("ERROR:", e.response?.data || e.message);
}
}
test();