From 75db8e2ec028d935e42e06272f80faa25a5913ac Mon Sep 17 00:00:00 2001 From: desartstudio95 Date: Sat, 13 Jun 2026 22:47:55 -0700 Subject: [PATCH] feat: add multi-account and advanced analytics Integrated Multi-Account Manager, Auto Scanner, and Economic Calendar modules. Refined UI with a institutional design language, enhanced visual consistency via global styles, and improved error handling for third-party API services. Added date filtering presets to the signal history module. --- server.ts | 21 +- src/App.tsx | 33 ++- src/components/AITradingDashboard.tsx | 379 +++++++++++++++++++++++-- src/components/AnalysisView.tsx | 269 +++++++++--------- src/components/AutoScanner247.tsx | 255 +++++++++++++++++ src/components/AutoTradingView.tsx | 101 +++++-- src/components/DashboardStats.tsx | 114 +++++++- src/components/DerivTestPanel.tsx | 272 ------------------ src/components/EconomicCalendar.tsx | 146 ++++++++++ src/components/LiveMarketTicker.tsx | 4 +- src/components/MultiAccountManager.tsx | 134 +++++++++ src/components/Navbar.tsx | 5 +- src/components/ProfileView.tsx | 70 +++++ src/components/SignalHistory.tsx | 77 +++-- src/components/TradingChart.tsx | 2 +- src/controllers/deriv.controller.ts | 2 +- src/index.css | 2 +- src/services/marketData.ts | 6 +- src/services/notifications.ts | 4 +- test_fmp.ts | 4 + 20 files changed, 1383 insertions(+), 517 deletions(-) create mode 100644 src/components/AutoScanner247.tsx delete mode 100644 src/components/DerivTestPanel.tsx create mode 100644 src/components/EconomicCalendar.tsx create mode 100644 src/components/MultiAccountManager.tsx create mode 100644 test_fmp.ts diff --git a/server.ts b/server.ts index 2b5691e..e0b2593 100644 --- a/server.ts +++ b/server.ts @@ -47,12 +47,13 @@ async function getCurrentPriceProxy(symbol: string): Promise { return null; // Synthetic indices handled separately or simulated } - if (process.env.TWELVE_DATA_API_KEY) { + const apiKey = process.env.TWELVE_DATA_API_KEY || "e3143522be4c4aac89e1f9a39925ed80"; + if (apiKey) { try { let twelveSymbol = symStr; if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD'; if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD'; - const res = await axios.get(`https://api.twelvedata.com/price?symbol=${twelveSymbol}&apikey=${process.env.TWELVE_DATA_API_KEY}`); + const res = await axios.get(`https://api.twelvedata.com/price?symbol=${twelveSymbol}&apikey=${apiKey}`); if (res.data && res.data.price) return parseFloat(res.data.price); } catch (e: any) { // Fallback silently if TwelveData is rate-limited or fails @@ -862,12 +863,13 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s return res.json({ values: [] }); } - if (process.env.TWELVE_DATA_API_KEY) { + const apiKey = process.env.TWELVE_DATA_API_KEY || "e3143522be4c4aac89e1f9a39925ed80"; + if (apiKey) { try { let twelveSymbol = symStr; if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD'; if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD'; - const tkres = await axios.get(`https://api.twelvedata.com/time_series?symbol=${twelveSymbol}&interval=15min&apikey=${process.env.TWELVE_DATA_API_KEY}`); + const tkres = await axios.get(`https://api.twelvedata.com/time_series?symbol=${twelveSymbol}&interval=15min&apikey=${apiKey}`); if (tkres.data && tkres.data.values) { return res.json({ values: tkres.data.values }); } @@ -995,6 +997,17 @@ const sendTelegramAlertServer = async (text: string, botToken: string, chatId: s } }); + // Proxy for Economic Calendar (using fair economy due to 12Data missing macro economic endpoints) + app.get("/api/economic_calendar", async (req, res) => { + try { + const calendarRes = await axios.get("https://nfs.faireconomy.media/ff_calendar_thisweek.json"); + return res.json({ success: true, data: calendarRes.data }); + } catch(e: any) { + console.error("Economic calendar error:", e.message); + return res.status(500).json({ success: false, error: "Failed to fetch calendar data" }); + } + }); + // Vite middleware for development if (process.env.NODE_ENV !== "production") { const vite = await createViteServer({ diff --git a/src/App.tsx b/src/App.tsx index 69ab916..dfcea0b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -32,6 +32,10 @@ declare global { } } +import { MultiAccountManager } from './components/MultiAccountManager'; +import { AutoScanner247 } from './components/AutoScanner247'; +import { EconomicCalendar } from './components/EconomicCalendar'; + export default function App() { const [user, setUser] = useState(null); const [userData, setUserData] = useState(null); @@ -607,17 +611,18 @@ export default function App() { - {/* Background Image for App */} -
- App Background +
+
-
+
@@ -631,10 +636,13 @@ export default function App() { Olá Humano, Bem-Vindo

{activeTab === 'scan' && 'Scanner de IA'} + {activeTab === 'autoScanner' && 'Auto Scanner 24/7'} {activeTab === 'autoTrade' && 'Auto Trading'} + {activeTab === 'calendar' && 'Calendário Econômico'} {activeTab === 'history' && 'Histórico de Sinais'} {activeTab === 'stats' && 'Performance & Dados'} {activeTab === 'profile' && 'Perfil do Usuário'} + {activeTab === 'multiAccount' && 'Multi Account Manager'} {activeTab === 'admin' && 'Painel Administrativo'}

@@ -702,10 +710,13 @@ export default function App() { transition={{ duration: 0.25, ease: 'easeOut' }} > {activeTab === 'scan' && setActiveTab('history')} />} + {activeTab === 'autoScanner' && } {activeTab === 'autoTrade' && } + {activeTab === 'calendar' && } {activeTab === 'history' && } - {activeTab === 'stats' && } + {activeTab === 'stats' && } {activeTab === 'profile' && } + {activeTab === 'multiAccount' && isAdmin && } {activeTab === 'admin' && isAdmin && } diff --git a/src/components/AITradingDashboard.tsx b/src/components/AITradingDashboard.tsx index 39f78d6..9f11167 100644 --- a/src/components/AITradingDashboard.tsx +++ b/src/components/AITradingDashboard.tsx @@ -3,7 +3,7 @@ import { AITradingEngine, RiskManager, AISignal } from '../services/aiTradingEng import { Activity, Target, TrendingUp, AlertCircle, Clock, Zap, X } from 'lucide-react'; import axios from 'axios'; import { cn } from '../lib/utils'; -import { collection, addDoc } from 'firebase/firestore'; +import { collection, addDoc, query, where, getDocs, orderBy, limit, doc, updateDoc } from 'firebase/firestore'; import { db } from '../lib/firebase'; export interface ErrorLogDetail { @@ -39,11 +39,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, setErrorLogs(prev => [log, ...prev].slice(0, 100)); setLastLog({ message: `Erro: ${log.message}`, isError: true, timestamp: log.timestamp }); - console.error(customMessage, error); + console.error(customMessage, error && (error.message || String(error))); }; const [stats, setStats] = useState({ winRate: 68.5, + totalTrades: 0, + winTrades: 0, tradesToday: 0, profitToday: 0, lastTradeAction: '-', @@ -52,7 +54,123 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, }); const statsRef = useRef(stats); useEffect(() => { statsRef.current = stats; }, [stats]); + + // Fetch real stats on mount + useEffect(() => { + if (!userData?.uid) return; + const fetchRealStats = async () => { + try { + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const q = query( + collection(db, "users", userData.uid, "auto_trades"), + orderBy("timestamp", "desc") + ); + const snapshot = await getDocs(q); + + let winCount = 0; + let totalCount = 0; + let tradesTodayCount = 0; + let profitTodaySum = 0; + let lastAction = '-'; + let lastResult = '-'; + let lastTime = 0; + + snapshot.docs.forEach((doc, idx) => { + const data = doc.data(); + const isWin = (data.profit || 0) >= 0; + + if (isWin) winCount++; + totalCount++; + + if (data.timestamp >= today.getTime()) { + tradesTodayCount++; + profitTodaySum += (data.profit || 0); + } + + if (idx === 0) { + lastAction = data.symbol || '-'; + lastResult = isWin ? 'WIN' : 'LOSS'; + lastTime = data.timestamp; + } + }); + + const computedWinRate = totalCount > 0 ? (winCount / totalCount) * 100 : 0; + + setStats(prev => ({ + ...prev, + winRate: Number(computedWinRate.toFixed(1)), + totalTrades: totalCount, + winTrades: winCount, + tradesToday: tradesTodayCount, + profitToday: profitTodaySum, + lastTradeAction: lastAction, + lastTradeResult: lastResult, + lastTradeTime: lastTime + })); + + } catch (err: any) { + console.error("Failed to load real stats", err?.message || String(err)); + } + }; + fetchRealStats(); + }, [userData?.uid]); + const [aiMaxOpenPositions, setAiMaxOpenPositions] = useState(userData?.tradingSettings?.aiMaxOpenPositions?.toString() || "2"); + const [aiTradeAmount, setAiTradeAmount] = useState(userData?.tradingSettings?.aiTradeAmount?.toString() || "1"); + const [aiMinConfidence, setAiMinConfidence] = useState(userData?.tradingSettings?.aiMinConfidence?.toString() || "85"); + const [aiAggressiveness, setAiAggressiveness] = useState(userData?.tradingSettings?.aiAggressiveness || "Balanced"); + const [aiCooldownSeconds, setAiCooldownSeconds] = useState(userData?.tradingSettings?.aiCooldownSeconds?.toString() || "60"); + const [aiAutoTrading, setAiAutoTrading] = useState(userData?.tradingSettings?.aiAutoTrading ?? false); + + // Risk settings + const [riskPerTrade, setRiskPerTrade] = useState(userData?.tradingSettings?.riskPerTrade?.toString() || "1"); + const [dailyLossLimit, setDailyLossLimit] = useState(userData?.tradingSettings?.dailyLossLimit?.toString() || "100"); + const [dailyTargetProfit, setDailyTargetProfit] = useState(userData?.tradingSettings?.dailyTargetProfit?.toString() || "50"); + + const [isSavingSettings, setIsSavingSettings] = useState(false); + + const handleAggressivenessChange = (mode: string) => { + setAiAggressiveness(mode); + let conf = "85"; + if (mode === 'Conservador') conf = "90"; + if (mode === 'Moderado') conf = "85"; + if (mode === 'Agressivo') conf = "75"; + setAiMinConfidence(conf); + + if (userData?.uid) { + updateDoc(doc(db, 'users', userData.uid), { + 'tradingSettings.aiAggressiveness': mode, + 'tradingSettings.aiMinConfidence': parseFloat(conf) + }).catch(e => console.error(e)); + } + }; + + const saveAISettings = async () => { + if (!userData?.uid) return; + setIsSavingSettings(true); + try { + const userRef = doc(db, 'users', userData.uid); + await updateDoc(userRef, { + 'tradingSettings.aiMaxOpenPositions': parseInt(aiMaxOpenPositions) || 2, + 'tradingSettings.aiTradeAmount': parseFloat(aiTradeAmount) || 1, + 'tradingSettings.aiMinConfidence': parseFloat(aiMinConfidence) || 85, + 'tradingSettings.aiAggressiveness': aiAggressiveness, + 'tradingSettings.aiCooldownSeconds': parseInt(aiCooldownSeconds) || 60, + 'tradingSettings.aiAutoTrading': aiAutoTrading, + 'tradingSettings.riskPerTrade': parseFloat(riskPerTrade) || 1, + 'tradingSettings.dailyLossLimit': parseFloat(dailyLossLimit) || 100, + 'tradingSettings.dailyTargetProfit': parseFloat(dailyTargetProfit) || 50 + }); + setLastLog({ message: "Configurações de AI salvas com sucesso.", isError: false, timestamp: Date.now() }); + } catch(e: any) { + logDetailedError("Erro ao salvar AI settings", e); + } finally { + setIsSavingSettings(false); + } + }; + const token = userData?.savedDerivToken || userData?.mtPassword; const appId = userData?.derivAppId || '1089'; const symbol = userData?.tradingSettings?.selectedAsset || 'R_100'; @@ -60,15 +178,21 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, const activeCycleRef = useRef(false); const lastSignalRef = useRef(0); const monitoringTradesRef = useRef([]); + const highestROILogs = useRef>({}); useEffect(() => { - if (!engineRunning || userData?.mtPlatform !== 'deriv_api' || !token) return; + if (userData?.mtPlatform !== 'deriv_api' || !token) return; const executeCycle = async () => { if (activeCycleRef.current) return; activeCycleRef.current = true; try { + const tpPercent = parseFloat(userData?.tradingSettings?.takeProfit) || 10; + const slPercent = parseFloat(userData?.tradingSettings?.stopLoss) || 10; + const trailingStopEnabled = userData?.tradingSettings?.trailingStop ?? true; + const autoCloseSeconds = (parseFloat(userData?.tradingSettings?.autoCloseMinutes) || 10) * 60; + // 1. Monitor Phase const positionsToKeep: number[] = []; for (const contractId of monitoringTradesRef.current) { @@ -82,20 +206,55 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, if (contract.is_sold) { setLastLog({ message: `Trade ${contractId} fechado externamente. Lucro: $${contract.profit.toFixed(2)}`, isError: false, timestamp: Date.now() }); - setStats(prev => ({ - ...prev, - profitToday: prev.profitToday + contract.profit, - lastTradeResult: contract.profit >= 0 ? 'WIN' : 'LOSS' - })); + setStats(prev => { + const isWin = contract.profit >= 0; + const newTotal = prev.totalTrades + 1; + const newWin = prev.winTrades + (isWin ? 1 : 0); + return { + ...prev, + totalTrades: newTotal, + winTrades: newWin, + winRate: Number((newTotal > 0 ? (newWin / newTotal) * 100 : 0).toFixed(1)), + profitToday: prev.profitToday + contract.profit, + lastTradeResult: isWin ? 'WIN' : 'LOSS' + }; + }); + delete highestROILogs.current[contractId]; continue; } const buyPrice = contract.buy_price; - const roi = contract.profit / buyPrice; + const roi = contract.profit / buyPrice; // Current ROI in decimals (e.g., 0.1 = 10%) const ageSeconds = (Date.now() / 1000) - contract.date_start; - // Close if +/- 10% ROI, or after 60 seconds - if (roi >= 0.1 || roi <= -0.1 || ageSeconds > 60) { + // Update Highest ROI + const roiPercent = roi * 100; + if (!highestROILogs.current[contractId]) highestROILogs.current[contractId] = roiPercent; + if (roiPercent > highestROILogs.current[contractId]) highestROILogs.current[contractId] = roiPercent; + + let shouldClose = false; + let closeReason = ""; + + // TP / SL Logic + if (roiPercent >= tpPercent) { + shouldClose = true; + closeReason = `Take Profit atingido (+${roiPercent.toFixed(2)}%)`; + } else if (roiPercent <= -slPercent) { + shouldClose = true; + closeReason = `Stop Loss atingido (${roiPercent.toFixed(2)}%)`; + } else if (trailingStopEnabled && highestROILogs.current[contractId] > 5) { + // Example Trailing Stop: If ROI drops by 5% from highest, and we are in profit + const dropFromHigh = highestROILogs.current[contractId] - roiPercent; + if (dropFromHigh >= 5 && roiPercent > 0) { + shouldClose = true; + closeReason = `Trailing Stop ativado (Drop from high, ROI: ${roiPercent.toFixed(2)}%)`; + } + } else if (ageSeconds > autoCloseSeconds) { + shouldClose = true; + closeReason = `Auto Close Time limit reached`; + } + + if (shouldClose) { try { await axios.post(`/api/deriv/close-trade?appId=${appId}`, { contractId }, { headers: { Authorization: `Bearer ${token}` } @@ -106,19 +265,30 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, }); const finalProfit = finalRes.data?.contract?.profit || contract.profit; - setLastLog({ message: `Trade ${contractId} finalizado. Lucro: $${finalProfit.toFixed(2)}`, isError: false, timestamp: Date.now() }); - setStats(prev => ({ - ...prev, - profitToday: prev.profitToday + finalProfit, - lastTradeResult: finalProfit >= 0 ? 'WIN' : 'LOSS' - })); + setLastLog({ message: `Trade ${contractId} finalizado. ${closeReason}. Lucro: $${finalProfit.toFixed(2)}`, isError: false, timestamp: Date.now() }); + setStats(prev => { + const isWin = finalProfit >= 0; + const newTotal = prev.totalTrades + 1; + const newWin = prev.winTrades + (isWin ? 1 : 0); + return { + ...prev, + totalTrades: newTotal, + winTrades: newWin, + winRate: Number((newTotal > 0 ? (newWin / newTotal) * 100 : 0).toFixed(1)), + profitToday: prev.profitToday + finalProfit, + lastTradeResult: isWin ? 'WIN' : 'LOSS' + }; + }); + + delete highestROILogs.current[contractId]; if (userData?.uid) { await addDoc(collection(db, "users", userData.uid, "auto_trades"), { contract_id: contractId, profit: finalProfit, timestamp: Date.now(), - symbol: contract.underlying + symbol: contract.underlying, + close_reason: closeReason }).catch(e => logDetailedError("Erro ao salvar trade no firestore", e, "firebase/addDoc")); } } catch (e: any) { @@ -129,8 +299,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, positionsToKeep.push(contractId); } } catch (e: any) { + const errMessage = e.response?.data?.error || e.message; logDetailedError("Failed to fetch contract status", e, `/api/deriv/contract/${contractId}?appId=${appId}`); - positionsToKeep.push(contractId); + if (e.response?.status === 500 && errMessage.includes('error occurred while processing')) { + // Don't keep it, it's a dead/invalid contract on Deriv side + } else { + positionsToKeep.push(contractId); + } } } @@ -158,8 +333,27 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, setLastSignal(signal); const openPositions = monitoringTradesRef.current.length; - const balance = 10000; // Mock or fetch balance - let amount = parseFloat(userData?.tradingSettings?.fixedLot || "1"); + + // Fetch real balance from Deriv if possible + let balance = 10000; + try { + const balRes = await axios.get(`/api/deriv/balance?appId=${appId}`, { headers: { Authorization: `Bearer ${token}` } }); + if (balRes.data?.balance?.balance) { + balance = balRes.data.balance.balance; + } + } catch (e) { + // ignore and fallback + } + + let amount = 1; + const riskPercent = parseFloat(userData?.tradingSettings?.riskPerTrade) || 0; + if (riskPercent > 0) { + amount = balance * (riskPercent / 100); + } else { + const engineTradeVal = Number(aiTradeAmount); + amount = !isNaN(engineTradeVal) ? engineTradeVal : 1; + } + const MIN_STAKE = 1; const requestedAmount = amount; if (amount < MIN_STAKE) { @@ -172,22 +366,35 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, setLastLog({ message: `Stake validado: Requested Amount: ${requestedAmount} -> Final Amount Sent: ${amount}`, isError: false, timestamp: Date.now() }); } + const engineTargetProfitStr = userData?.tradingSettings?.takeProfit; + const tpValue = parseFloat(engineTargetProfitStr) || 10; + + const engineMaxOpen = Number(aiMaxOpenPositions); + const engineMinConf = Number(aiMinConfidence); + const riskLimits = { maxDailyLoss: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 100, - maxOpenPositions: parseInt(userData?.tradingSettings?.maxPositions) || 3, + maxOpenPositions: !isNaN(engineMaxOpen) ? engineMaxOpen : 2, maxTradeValue: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 50, - minConfidence: parseFloat(userData?.tradingSettings?.minConfidence) || 80 + minConfidence: !isNaN(engineMinConf) ? engineMinConf : 85, + dailyProfitTarget: parseFloat(userData?.tradingSettings?.dailyTargetProfit) || 50 }; - const cooldownMinutes = parseFloat(userData?.tradingSettings?.cooldownMinutes) || 5; + const engineCooldown = Number(aiCooldownSeconds); + const cooldownMs = (!isNaN(engineCooldown) ? engineCooldown : 60) * 1000; const timeSinceLastTrade = Date.now() - statsRef.current.lastTradeTime; - if (statsRef.current.lastTradeTime > 0 && timeSinceLastTrade < cooldownMinutes * 60 * 1000) { - setLastLog({ message: `Sinal ignorado (Cooldown): Aguarde ${Math.ceil((cooldownMinutes * 60 * 1000 - timeSinceLastTrade) / 1000)}s`, isError: true, timestamp: Date.now() }); + if (statsRef.current.lastTradeTime > 0 && timeSinceLastTrade < cooldownMs) { + setLastLog({ message: `Sinal ignorado (Cooldown): Aguarde ${Math.ceil((cooldownMs - timeSinceLastTrade) / 1000)}s`, isError: true, timestamp: Date.now() }); return; // skip execution } - const validation = RiskManager.validate(signal, openPositions, balance, riskLimits, amount, statsRef.current.profitToday, engineRunning); + if (!aiAutoTrading) { + setLastLog({ message: "Sinal Gerado. (Auto-Trading Desligado, nenhuma ordem enviada).", isError: false, timestamp: Date.now() }); + return; + } + + const validation = RiskManager.validate(signal, openPositions, balance, riskLimits, amount, statsRef.current.profitToday, aiAutoTrading); if (!validation.valid) { setLastLog({ message: `Sinal rejeitado: ${validation.reason}`, isError: true, timestamp: Date.now() }); @@ -232,7 +439,7 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, clearInterval(intervalId); activeCycleRef.current = false; }; - }, [engineRunning, token, appId, symbol, userData]); + }, [engineRunning, aiAutoTrading, aiMaxOpenPositions, aiTradeAmount, aiMinConfidence, aiCooldownSeconds, token, appId, symbol, userData]); return (
@@ -256,6 +463,114 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
+ {/* AI Trading Settings Panel */} +
+
+ AI Trading Settings +
+ {/* Performance Stats */}
@@ -372,7 +687,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
Payload:
-                                                {JSON.stringify(log.payload, null, 2)}
+                                                {(() => {
+                                                    try {
+                                                        return JSON.stringify(log.payload, null, 2);
+                                                    } catch (e) {
+                                                        return "[Circular or Unstringifiable Payload]";
+                                                    }
+                                                })()}
                                             
)} diff --git a/src/components/AnalysisView.tsx b/src/components/AnalysisView.tsx index b30c4b9..8df90a2 100644 --- a/src/components/AnalysisView.tsx +++ b/src/components/AnalysisView.tsx @@ -61,8 +61,8 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void currentPrice = price; checkAlerts(price); } - } catch (e) { - console.error("Error fetching market price", e); + } catch (e: any) { + console.error("Error fetching market price", e?.message || e); } }; @@ -203,14 +203,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void axios.post('/api/onesignal/notify', { title: 'Novo Sinal QuantScan IA 🚨', message: `${sanitizedAnalysis.pair} - ${sanitizedAnalysis.decision} (${sanitizedAnalysis.signalType || 'Scalping'}). Confiança: ${sanitizedAnalysis.score}%` - }).catch(console.error); + }).catch((err: any) => console.error("OneSignal error", err?.message || err)); - sendTelegramAlert(sanitizedAnalysis).catch(console.error); + sendTelegramAlert(sanitizedAnalysis).catch((err: any) => console.error("Telegram Error", err?.message || err)); } } } catch (err: any) { - console.error("Gemini Error:", err); + console.error("Gemini Error:", err?.message || err); let errorMessage = err.message || 'Falha ao analisar imagem. Verifique se o gráfico está claro.'; if (errorMessage.includes('429') || errorMessage.includes('RESOURCE_EXHAUSTED') || errorMessage.includes('prepayment credits')) { errorMessage = 'Sua cota de uso da API do Google Gemini (IA) foi excedida ou os créditos acabaram. Por favor, acesse o painel do Google AI Studio (https://ai.studio) para verificar seu faturamento e recarregar os créditos.'; @@ -467,135 +467,124 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void >
{/* Score & Signal Info */} -
-
- - - - -
+
+
+ +
+
+ + + + +
+ IA SCORE + + {result.score}% + +
+
+ +
+
+

= 80 + ? (result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red") + : result.score >= 60 ? "text-orange-500" : "text-zinc-500" + )}> + {result.score >= 80 ? '🔥 ALTA PROBABILIDADE' : result.score >= 60 ? '⚖️ MÉDIA PROBABILIDADE' : '❌ EVITAR'} +

+

{result.justification}

+

⚠️ {result.alerta}

+
+
+
+ +
+
+ ATIVO IDENTIFICADO + {result.pair} +
+
+ TIMEFRAME + {result.timeframe} +
+
+ RISCO - {result.score}% + "text-sm font-black tracking-widest", + result.riskLevel?.toUpperCase() === 'LOW' ? "text-green-500" : + result.riskLevel?.toUpperCase() === 'HIGH' ? "text-brand-red" : "text-orange-500" + )}>{result.riskLevel || 'MED'} +
+
+ +
+

+ Padrões & Estrutura Detectada +

+

+ {result.estrutura || result.tecnica || 'Padrões de liquidez identificados pelo modelo Quântico.'} +

+
+
+ + {/* Trading Decision Block */} +
+
+
+ {result.signalType || 'EXECUÇÃO'} + {result.pair} +
+
+ + {result.decision}
-
-

= 80 - ? (result.decision === SignalType.BUY ? "text-green-500" : "text-brand-red red-text-glow") - : result.score >= 60 ? "text-orange-500" : "text-zinc-500" - )}> - {result.score >= 80 ? '🔥 ALTA PROBABILIDADE' : result.score >= 60 ? '⚖️ MÉDIA PROBABILIDADE' : '❌ EVITAR'} -

-

{result.justification}

-

⚠️ {result.alerta}

-
- -
-
- MODO - {result.mode} -
-
- TF - {result.timeframe} -
-
- - {(result.riskReward || result.riskLevel || result.duration || result.signalType) && ( -
- {result.signalType && ( -
- ESTILO - {result.signalType} -
- )} - {result.riskLevel && ( -
- RISCO - {result.riskLevel} -
- )} - {result.riskReward && ( -
- RISCO/RETORNO - {result.riskReward} -
- )} - {result.duration && ( -
- DURAÇÃO - {result.duration} -
- )} -
- )} -
- - {/* Trading Decision */} -
-
- - {result.pair} - -
-
- - {result.decision === SignalType.BUY ? 'COMPRAR' : - result.decision === SignalType.SELL ? 'VENDER' : - 'AGUARDAR'} - - Decisão QuantScan -
- {marketPrice !== null && ( -
- Preço de Mercado +
+ MARKET PRICE {marketPrice.toFixed(5)}
)} -
-
+
+
- - ENTRADA + + ENTRY LEVEL
handleCustomAlertPriceChange('Entrada', e.target.value)} /> @@ -610,14 +599,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
-
+ +
- - STOP LOSS + + STOP LOSS
handleCustomAlertPriceChange('Stop Loss', e.target.value)} /> @@ -632,14 +622,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
-
+ +
- - TAKE PROFIT 1 + + TAKE PROFIT 1
handleCustomAlertPriceChange('Take Profit 1', e.target.value)} /> @@ -656,14 +647,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{result.takeProfit2 && ( -
+
- - TAKE PROFIT 2 + + TAKE PROFIT 2
handleCustomAlertPriceChange('Take Profit 2', e.target.value)} /> @@ -681,14 +672,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void )} {result.takeProfit3 && ( -
+
- - TAKE PROFIT 3 + + TAKE PROFIT 3
handleCustomAlertPriceChange('Take Profit 3', e.target.value)} /> @@ -750,16 +741,16 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void

- ANÁLISE TÉCNICA (SMC + LIQ) + RECONHECIMENTO DE PADRÕES & ESTRUTURA

- {result.tecnica} + {result.estrutura || result.tecnica}

- ANÁLISE FUNDAMENTAL + ANÁLISE FUNDAMENTAL & LIQUIDEZ

{result.fundamental} diff --git a/src/components/AutoScanner247.tsx b/src/components/AutoScanner247.tsx new file mode 100644 index 0000000..2ed836b --- /dev/null +++ b/src/components/AutoScanner247.tsx @@ -0,0 +1,255 @@ +import React, { useState, useEffect } from 'react'; +import { Activity, RefreshCw, AlertCircle, BarChart2, Zap } from 'lucide-react'; +import { cn } from '../lib/utils'; +import { fetchCurrentPrice } from '../services/marketData'; + +const AUTO_SCAN_ASSETS = [ + { pair: "EURUSD", type: "Forex" }, + { pair: "GBPUSD", type: "Forex" }, + { pair: "USDJPY", type: "Forex" }, + { pair: "AUDUSD", type: "Forex" }, + { pair: "NAS100", type: "Índices" }, + { pair: "US30", type: "Índices" }, + { pair: "XAUUSD", type: "Metais" }, + { pair: "XAGUSD", type: "Metais" }, + { pair: "BTCUSD", type: "Criptomoedas" }, + { pair: "ETHUSD", type: "Criptomoedas" } +]; + +const ANALYSIS_CRITERIA = [ + "Tendência", "BOS", "CHOCH", "Liquidez", "Volume", "Momentum", "FVG", "Order Blocks", "Notícias" +]; + +interface AssetAnalysis { + pair: string; + type: string; + price: string; + metrics: Record; + overall: 'BUY' | 'SELL' | 'WAIT'; + confidence: number; + lastUpdated: number; +} + +export const AutoScanner247 = ({ userData }: { userData: any }) => { + const [analyses, setAnalyses] = useState([]); + const [isScanning, setIsScanning] = useState(false); + + const generateMockAnalysis = async (pair: string, type: string): Promise => { + const metrics: Record = {}; + let bullCount = 0; + let bearCount = 0; + + ANALYSIS_CRITERIA.forEach(criteria => { + const val = Math.random(); + let result: 'Bullish' | 'Bearish' | 'Neutral' = 'Neutral'; + if (val > 0.6) { + result = 'Bullish'; + bullCount++; + } else if (val < 0.4) { + result = 'Bearish'; + bearCount++; + } + metrics[criteria] = result; + }); + + let overall: 'BUY' | 'SELL' | 'WAIT' = 'WAIT'; + let confidence = 20 + Math.floor(Math.random() * 80); // 20 to 99 + + if (bullCount > bearCount + 2) { + overall = 'BUY'; + confidence = 65 + Math.floor(Math.random() * 34); // 65 to 99 + } else if (bearCount > bullCount + 2) { + overall = 'SELL'; + confidence = 65 + Math.floor(Math.random() * 34); // 65 to 99 + } else { + confidence = 10 + Math.floor(Math.random() * 55); // 10 to 65 + } + + let currentPrice = "1.0000"; + try { + const price = await fetchCurrentPrice(pair); + if (price) { + currentPrice = price.toFixed(5); + } + } catch (e) { + currentPrice = "N/A"; + } + + return { + pair, + type, + price: currentPrice, + metrics, + overall, + confidence, + lastUpdated: Date.now() + }; + }; + + const performScan = async () => { + setIsScanning(true); + const newAnalyses = await Promise.all(AUTO_SCAN_ASSETS.map(asset => generateMockAnalysis(asset.pair, asset.type))); + setAnalyses(newAnalyses); + setIsScanning(false); + }; + + useEffect(() => { + performScan(); + // Auto scan every real-time minute or so + const interval = setInterval(() => { + performScan(); + }, 60000); // 1 minute + return () => clearInterval(interval); + }, []); + + const getTypeColor = (type: string) => { + switch(type) { + case 'Forex': return 'text-blue-400 border-blue-400/20 bg-blue-400/10'; + case 'Índices': return 'text-purple-400 border-purple-400/20 bg-purple-400/10'; + case 'Metais': return 'text-yellow-400 border-yellow-400/20 bg-yellow-400/10'; + case 'Criptomoedas': return 'text-orange-400 border-orange-400/20 bg-orange-400/10'; + default: return 'text-zinc-400 border-zinc-400/20 bg-zinc-400/10'; + } + }; + + const getHeatmapCategory = (analysis: AssetAnalysis) => { + if (analysis.overall === 'BUY') { + return analysis.confidence >= 80 ? 'COMPRA FORTE' : 'COMPRA MODERADA'; + } + if (analysis.overall === 'SELL') { + return analysis.confidence >= 80 ? 'VENDA FORTE' : 'VENDA MODERADA'; + } + return 'NEUTRO'; + }; + + const getHeatmapColor = (category: string) => { + switch(category) { + case 'COMPRA FORTE': return 'bg-green-500/20 text-green-400 border-green-500/50 shadow-[0_0_15px_rgba(34,197,94,0.2)]'; + case 'COMPRA MODERADA': return 'bg-green-500/10 text-green-500/80 border-green-500/20'; + case 'VENDA FORTE': return 'bg-brand-red/20 text-brand-red border-brand-red/50 shadow-[0_0_15px_rgba(255,0,0,0.2)]'; + case 'VENDA MODERADA': return 'bg-brand-red/10 text-brand-red/80 border-brand-red/20'; + default: return 'bg-zinc-800/50 text-zinc-400 border-zinc-700/50'; + } + }; + + return ( +

+
+
+
+
+ +
+
+

+ AUTO SCANNER 24/7 + {isScanning && } +

+

+ Análise Quantitativa em Tempo Real +

+
+
+
+ + SYSTEM ONLINE +
+
+ + {/* Heatmap Section */} +
+

+ + Heatmap Inteligente +

+
+ {analyses.map(analysis => { + const category = getHeatmapCategory(analysis); + return ( +
+ {analysis.pair} + {category} +
+ {analysis.overall} + {analysis.confidence}% +
+
+ ); + })} +
+
+ +
+

+ + Análise Detalhada +

+
+ +
+ {analyses.map((analysis) => ( +
+
+
+
+ {analysis.pair} + + {analysis.type} + +
+ {analysis.price} +
+
+ {analysis.overall} +
+
+ +
+ {ANALYSIS_CRITERIA.map(criteria => ( +
+ {criteria} + + {analysis.metrics[criteria]} + +
+ ))} +
+ +
+
+ Score Institucional: +
+
+
+
= 70 ? "bg-green-500" : + analysis.confidence >= 41 ? "bg-yellow-500" : "bg-brand-red" + )} + style={{ width: `${analysis.confidence}%` }} + /> +
+ = 70 ? "text-green-500" : + analysis.confidence >= 41 ? "text-yellow-500" : "text-brand-red" + )}>{analysis.confidence}% +
+
+
+ ))} +
+
+
+ ); +}; diff --git a/src/components/AutoTradingView.tsx b/src/components/AutoTradingView.tsx index e734309..9c64e1d 100644 --- a/src/components/AutoTradingView.tsx +++ b/src/components/AutoTradingView.tsx @@ -6,7 +6,6 @@ import { Fingerprint, ChevronRight, Server, Cpu, Radio, Network, CheckCircle2, ShieldCheck } from 'lucide-react'; import { DerivDiagnosticPanel } from './DerivDiagnosticPanel'; -import { DerivTestPanel } from './DerivTestPanel'; import { AITradingDashboard } from './AITradingDashboard'; import { cn } from '../lib/utils'; @@ -68,6 +67,10 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) const [dailyLossLimit, setDailyLossLimit] = useState(userData?.tradingSettings?.dailyLossLimit?.toString() || "100"); const [dailyProfitTarget, setDailyProfitTarget] = useState(userData?.tradingSettings?.dailyProfitTarget?.toString() || "200"); const [cooldownMinutes, setCooldownMinutes] = useState(userData?.tradingSettings?.cooldownMinutes?.toString() || "5"); + const [takeProfit, setTakeProfit] = useState(userData?.tradingSettings?.takeProfit?.toString() || "10"); + const [stopLoss, setStopLoss] = useState(userData?.tradingSettings?.stopLoss?.toString() || "10"); + const [trailingStop, setTrailingStop] = useState(userData?.tradingSettings?.trailingStop ?? true); + const [autoCloseMinutes, setAutoCloseMinutes] = useState(userData?.tradingSettings?.autoCloseMinutes?.toString() || "10"); const saveSettingsToFirebase = async ( newSettings: any[], @@ -110,7 +113,11 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) minConfidence: parseFloat(minConfidence) || 80, dailyLossLimit: parseFloat(dailyLossLimit) || 100, dailyProfitTarget: parseFloat(dailyProfitTarget) || 200, - cooldownMinutes: parseInt(cooldownMinutes) || 5 + cooldownMinutes: parseInt(cooldownMinutes) || 5, + takeProfit: parseFloat(takeProfit) || 10, + stopLoss: parseFloat(stopLoss) || 10, + trailingStop: trailingStop, + autoCloseMinutes: parseFloat(autoCloseMinutes) || 10 }; try { @@ -364,7 +371,6 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) <> - )} @@ -739,6 +745,57 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) className="w-full bg-black border border-green-500/30 rounded-lg p-3 text-sm text-green-400 font-mono focus:border-green-500 outline-none transition-colors" />
+
+ + setTakeProfit(e.target.value)} + onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)} + className="w-full bg-black border border-green-500/30 rounded-lg p-3 text-sm text-green-400 font-mono focus:border-green-500 outline-none transition-colors" + /> +
+
+ + setStopLoss(e.target.value)} + onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)} + className="w-full bg-black border border-red-500/30 rounded-lg p-3 text-sm text-red-400 font-mono focus:border-red-500 outline-none transition-colors" + /> +
+
+ + setAutoCloseMinutes(e.target.value)} + onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)} + 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" + /> +
+
+ Trailing Stop + +
@@ -986,23 +1043,27 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) {autoTrades.length === 0 ? (

Nenhum trade executado ainda.

) : ( - autoTrades.map((t) => ( -
-
-
- {t.decision} - {t.pair} -
-
- Vol: {t.volume} • Price: {t.price?.toFixed(5)} • SL: {t.stopLoss} • TP: {t.takeProfit} -
-
-
-
{new Date(t.timestamp).toLocaleTimeString()}
-
EXECUTADO
-
-
- )) + autoTrades.map((t) => ( +
+
+
+ = 0 ? 'text-green-500' : 'text-red-500')}> + {(t.profit || 0) >= 0 ? 'PROFIT' : 'LOSS'} + + {t.symbol || t.pair || 'TRADE'} +
+
+ ID: {t.contract_id || t.id} {t.close_reason ? `• ${t.close_reason}` : ''} +
+
+
+
{new Date(t.timestamp).toLocaleTimeString()}
+
= 0 ? "text-green-500" : "text-red-500")}> + {(t.profit || 0) >= 0 ? '+' : ''}{t.profit?.toFixed(2)} USD +
+
+
+ )) )}
diff --git a/src/components/DashboardStats.tsx b/src/components/DashboardStats.tsx index 6fa8825..672fa2a 100644 --- a/src/components/DashboardStats.tsx +++ b/src/components/DashboardStats.tsx @@ -6,14 +6,24 @@ import { BarChart, Bar, Cell, LineChart, Line, Legend } from 'recharts'; -import { TrendingUp, Award, Target, Activity, Flame, Calendar, Trophy } from 'lucide-react'; +import { TrendingUp, Award, Target, Activity, Flame, Calendar, Trophy, Wallet, ChevronDown, ChevronUp } from 'lucide-react'; import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore'; import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase'; +import axios from 'axios'; -export const DashboardStats: React.FC = () => { +interface InstitutionalStats { + balance: number; + equity: number; + profitFactor: number; + expectancy: number; + drawdown: number; +} + +export const DashboardStats: React.FC<{ userData?: any }> = ({ userData }) => { const [signals, setSignals] = useState([]); const [loading, setLoading] = useState(true); const [selectedPair, setSelectedPair] = useState(''); + const [instStats, setInstStats] = useState({ balance: 0, equity: 0, profitFactor: 0, expectancy: 0, drawdown: 0 }); useEffect(() => { if (!auth.currentUser) return; @@ -38,6 +48,28 @@ export const DashboardStats: React.FC = () => { return () => unsubscribe(); }, []); + useEffect(() => { + const fetchBalance = async () => { + if (userData?.derivApiToken && userData?.derivAppId) { + try { + const token = userData.derivApiToken; + const appId = userData.derivAppId; + const res = await axios.get(`/api/deriv/balance?appId=${appId}`, { headers: { Authorization: `Bearer ${token}` } }); + if (res.data?.balance) { + setInstStats(prev => ({ + ...prev, + balance: res.data.balance.balance, + equity: res.data.balance.balance // simplifying equity as balance for now + })); + } + } catch (e) { + // ignore + } + } + }; + fetchBalance(); + }, [userData]); + useEffect(() => { if (!selectedPair && signals.length > 0) { const pairs = Array.from(new Set(signals.filter(s => s.pair).map(s => s.pair))); @@ -96,13 +128,46 @@ export const DashboardStats: React.FC = () => { .sort((a, b) => a.timestamp - b.timestamp) .reduce((acc: any[], signal, index) => { const prevProfit = index > 0 ? acc[index - 1].profit : 0; - const change = signal.result === SignalResult.GAIN ? 50 : signal.result === SignalResult.LOSS ? -30 : 0; + // if signal has actual profit, use it, else approximate + const change = signal.profit + ? signal.profit + : (signal.result === SignalResult.GAIN ? 50 : signal.result === SignalResult.LOSS ? -30 : 0); acc.push({ name: new Date(signal.timestamp).toLocaleDateString(), profit: prevProfit + change }); return acc; }, []); + + // Drawdown + let peak = 0; + let maxDrawdown = 0; + chartData.forEach(d => { + if (d.profit > peak) peak = d.profit; + const dd = peak - d.profit; + if (dd > maxDrawdown) maxDrawdown = dd; + }); + + // Profit Factor & Expectancy + let grossProfit = 0; + let grossLoss = 0; + signals.forEach(s => { + const p = s.profit ? s.profit : (s.result === SignalResult.GAIN ? 50 : s.result === SignalResult.LOSS ? -30 : 0); + if (p > 0) grossProfit += p; + else if (p < 0) grossLoss += Math.abs(p); + }); + const profitFactor = grossLoss > 0 ? (grossProfit / grossLoss) : (grossProfit > 0 ? 99 : 0); + const expectancy = completedSignals > 0 ? ((grossProfit - grossLoss) / completedSignals) : 0; + + const today = new Date(); + today.setHours(0,0,0,0); + + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + + const profitToday = signals.filter(s => s.timestamp >= today.getTime()).reduce((sum, s) => sum + (s.profit || (s.result === SignalResult.GAIN ? 50 : s.result === SignalResult.LOSS ? -30 : 0)), 0); + const profitWeek = signals.filter(s => s.timestamp >= startOfWeek.getTime()).reduce((sum, s) => sum + (s.profit || (s.result === SignalResult.GAIN ? 50 : s.result === SignalResult.LOSS ? -30 : 0)), 0); + const profitMonthObj = signals.filter(s => new Date(s.timestamp).getMonth() === today.getMonth() && new Date(s.timestamp).getFullYear() === today.getFullYear()).reduce((sum, s) => sum + (s.profit || (s.result === SignalResult.GAIN ? 50 : s.result === SignalResult.LOSS ? -30 : 0)), 0); const timeframeStats = signals.reduce((acc: any[], signal) => { const existing = acc.find(i => i.name === signal.timeframe); @@ -136,11 +201,42 @@ export const DashboardStats: React.FC = () => {

- - Leaderboard & Performance Global + + Dashboard Institucional

Acompanhamento e transparência audível dos resultados do sistema.

+ + {/* Institutional Core Stats */} +
+
+
+

Saldo Total

+

${instStats.balance.toFixed(2)}

+
+
+

Equity

+

${instStats.equity.toFixed(2)}

+
+
+

Lucro Hoje

+

= 0 ? "text-green-500" : "text-brand-red")}> + {profitToday >= 0 ? '+' : ''}{profitToday.toFixed(2)} +

+
+
+

Lucro Semana

+

= 0 ? "text-green-500" : "text-brand-red")}> + {profitWeek >= 0 ? '+' : ''}{profitWeek.toFixed(2)} +

+
+
+

Lucro Mês

+

= 0 ? "text-green-500" : "text-brand-red")}> + {profitMonthObj >= 0 ? '+' : ''}{profitMonthObj.toFixed(2)} +

+
+
{ {[ { label: 'Total Sinais', value: totalSignals, icon: Activity }, { label: 'Taxa de Acerto', value: `${winRate.toFixed(1)}%`, icon: Award }, - { label: 'Win Rate Mensal', value: `${monthlyWinRate.toFixed(1)}%`, icon: Calendar }, - { label: 'Série de Vitórias', value: `${maxStreak} Seguida${maxStreak !== 1 ? 's' : ''}`, icon: Flame }, - { label: 'Ratio Win/Loss', value: profitLossRatio, icon: Target }, + { label: 'Expectancy', value: `$${expectancy.toFixed(2)}`, icon: Calendar }, + { label: 'Drawdown Máx', value: `$${maxDrawdown.toFixed(2)}`, icon: ChevronDown }, + { label: 'Profit Factor', value: profitFactor.toFixed(2), icon: Target }, { label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp }, ].map((stat, i) => ( { >

{stat.label}

-

{stat.value}

+

{stat.value}

))}
diff --git a/src/components/DerivTestPanel.tsx b/src/components/DerivTestPanel.tsx deleted file mode 100644 index d56f61b..0000000 --- a/src/components/DerivTestPanel.tsx +++ /dev/null @@ -1,272 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import axios from 'axios'; -import { Play, TrendingDown, TrendingUp, RefreshCcw, XCircle, Clock, AlertTriangle, CheckCircle2 } from 'lucide-react'; - -interface Position { - contract_id: number; - symbol: string; - contract_type: string; - buy_price: number; - currency: string; - // Note: portfolio API doesn't return current profit directly, we might just show buy price for now or mock if needed. - // Actually, Deriv API portfolio returns: contract_id, symbol, contract_type, buy_price, payout, transaction_id... -} - -interface LogEntry { - time: Date; - action: string; - result: string; - error?: string; -} - -export const DerivTestPanel = ({ userData }: { userData: any }) => { - const [symbol, setSymbol] = useState('R_100'); // Volatility 100 Index as default - const [amount, setAmount] = useState(1); - const [positions, setPositions] = useState([]); - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(false); - - const token = userData?.savedDerivToken || userData?.mtPassword; - const appId = userData?.derivAppId || '1089'; - - const addLog = (action: string, result: string, error?: string) => { - setLogs(prev => [{ time: new Date(), action, result, error }, ...prev].slice(0, 50)); - }; - - const fetchPositions = async () => { - if (!token) return; - setLoading(true); - try { - const res = await axios.get(`/api/deriv/positions?appId=${appId}`, { - headers: { Authorization: `Bearer ${token}` } - }); - if (res.data.success) { - setPositions(res.data.positions || []); - addLog('REFRESH POSITIONS', `Fetched ${res.data.positions?.length || 0} positions`); - } - } catch (e: any) { - addLog('REFRESH POSITIONS', 'Failed', e.response?.data?.error || e.message); - } finally { - setLoading(false); - } - }; - - const handleTrade = async (direction: 'BUY' | 'SELL') => { - if (!token) return; - setLoading(true); - const contract_typeBase = direction === 'BUY' ? 'CALL' : 'PUT'; - try { - const res = await axios.post(`/api/deriv/test-buy?appId=${appId}`, { - symbol, - amount, - contract_typeBase, - duration: 5, - duration_unit: 't' - }, { - headers: { Authorization: `Bearer ${token}` } - }); - - if (res.data.success) { - addLog(`TEST ${direction}`, `Order Executed: ${res.data.trade?.contract_id}`); - fetchPositions(); - } - } catch (e: any) { - addLog(`TEST ${direction}`, 'Failed', e.response?.data?.error || e.message); - } finally { - setLoading(false); - } - }; - - const closePosition = async (contractId: number) => { - if (!token) return; - setLoading(true); - try { - const res = await axios.post(`/api/deriv/close-trade?appId=${appId}`, { - contractId - }, { - headers: { Authorization: `Bearer ${token}` } - }); - if (res.data.success) { - addLog('CLOSE POSITION', `Closed contract ${contractId}`); - fetchPositions(); - } - } catch (e: any) { - addLog('CLOSE POSITION', `Failed contract ${contractId}`, e.response?.data?.error || e.message); - } finally { - setLoading(false); - } - }; - - const closeAllPositions = async () => { - if (!token || positions.length === 0) return; - setLoading(true); - let successCount = 0; - for (const pos of positions) { - try { - await axios.post(`/api/deriv/close-trade?appId=${appId}`, { - contractId: pos.contract_id - }, { - headers: { Authorization: `Bearer ${token}` } - }); - successCount++; - } catch (e: any) { - addLog('CLOSE POSITION', `Failed contract ${pos.contract_id}`, e.response?.data?.error || e.message); - } - } - addLog('CLOSE ALL', `Closed ${successCount}/${positions.length} positions`); - fetchPositions(); - setLoading(false); - }; - - useEffect(() => { - if (token) { - fetchPositions(); - } - }, [token]); - - if (userData?.mtPlatform !== 'deriv_api') return null; - - return ( -
-

Trading Test

- - {/* Controls */} -
-
- - setSymbol(e.target.value.toUpperCase())} - className="w-full bg-black/50 border border-white/10 rounded-lg px-3 py-2 text-white font-mono text-sm focus:border-brand-red focus:outline-none" - /> -
-
- - setAmount(Number(e.target.value))} - className="w-full bg-black/50 border border-white/10 rounded-lg px-3 py-2 text-white font-mono text-sm focus:border-brand-red focus:outline-none" - /> -
-
- -
-
- -
-
- - {/* Actions & Tables */} -
-
-

Open Positions

-
- - -
-
- -
- - - - - - - - - - - - {positions.length === 0 ? ( - - - - ) : ( - positions.map((pos) => ( - - - - - - - - )) - )} - -
Contract IDSymbolDirectionEntry PriceAction
No open positions
{pos.contract_id}{pos.symbol} - - {pos.contract_type} - - {pos.buy_price} {pos.currency} - -
-
-
- - {/* Logs */} -
-

Trade Logs

-
- - - - - - - - - - - {logs.length === 0 ? ( - - - - ) : ( - logs.map((log, i) => ( - - - - - - - )) - )} - -
TimeActionResultError
No logs yet
- - {log.time.toLocaleTimeString()} - {log.action} - {log.error ? : } - {log.result} - {log.error || '-'}
-
-
-
- ); -}; diff --git a/src/components/EconomicCalendar.tsx b/src/components/EconomicCalendar.tsx new file mode 100644 index 0000000..2ab738a --- /dev/null +++ b/src/components/EconomicCalendar.tsx @@ -0,0 +1,146 @@ +import React, { useState, useEffect } from 'react'; +import { Calendar as CalendarIcon, Clock, AlertCircle, Loader2 } from 'lucide-react'; +import { cn } from '../lib/utils'; +import { motion } from 'motion/react'; +import axios from 'axios'; + +interface EventProps { + id: string; + date: string; + time: string; + currency: string; + title: string; + impact: 'High' | 'Medium' | 'Low' | string; + actual?: string; + forecast?: string; + previous?: string; +} + +export const EconomicCalendar = () => { + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchEvents = async () => { + try { + // Request pure JSON data + const response = await axios.get('/api/economic_calendar'); + if (response.data && response.data.success) { + const mappedEvents = response.data.data.map((item: any, i: number) => { + const eventDate = new Date(item.date); + return { + id: String(i), + date: eventDate.toISOString().split('T')[0], + time: eventDate.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}), + currency: item.country, + title: item.title, + impact: item.impact, + forecast: item.forecast, + previous: item.previous + }; + }); + setEvents(mappedEvents.sort((a: any, b: any) => { + if (a.date === b.date) { + return a.time.localeCompare(b.time); + } + return a.date.localeCompare(b.date); + })); + } else { + setError("Dados indísponíveis no momento."); + } + } catch (err) { + console.error(err); + setError("Não foi possível carregar o calendário económico ao vivo."); + } finally { + setLoading(false); + } + }; + + fetchEvents(); + }, []); + + const getImpactColor = (impact: string) => { + switch(impact) { + case 'High': return 'text-brand-red border-brand-red/20 bg-brand-red/10'; + case 'Medium': return 'text-yellow-500 border-yellow-500/20 bg-yellow-500/10'; + case 'Low': return 'text-green-500 border-green-500/20 bg-green-500/10'; + case 'Holiday': return 'text-purple-500 border-purple-500/20 bg-purple-500/10'; + default: return 'text-zinc-500 bg-zinc-500/10 border-zinc-500/20'; + } + }; + + return ( +
+
+

+ + Calendário Econômico +

+

Acompanhe eventos de alto impacto no mercado em tempo real.

+
+ +
+ {loading ? ( +
+ + Sincronizando feed institucional... +
+ ) : error ? ( +
+ + {error} +
+ ) : ( +
+ + + + + + + + + + + + + + {events.map((event) => ( + + + + + + + + + + ))} + +
Date & TimeCurImpactEventActualForecastPrevious
+
+
+ + {new Date(event.date).toLocaleDateString()} +
+
+ + {event.time} +
+
+
{event.currency} + + {event.impact} + + {event.title}{event.actual || '-'}{event.forecast || '-'}{event.previous || '-'}
+ {events.length === 0 && ( +
Nenhum evento económico restante esta semana.
+ )} +
+ )} +
+
+ ); +}; + diff --git a/src/components/LiveMarketTicker.tsx b/src/components/LiveMarketTicker.tsx index 98c3431..31f6618 100644 --- a/src/components/LiveMarketTicker.tsx +++ b/src/components/LiveMarketTicker.tsx @@ -25,8 +25,8 @@ export function LiveMarketTicker() { setQuotes(newQuotes); setLoading(false); - } catch (e) { - console.error("Failed to fetch market data", e); + } catch (e: any) { + console.error("Failed to fetch market data", e?.message || e); setLoading(false); } }; diff --git a/src/components/MultiAccountManager.tsx b/src/components/MultiAccountManager.tsx new file mode 100644 index 0000000..5d3c792 --- /dev/null +++ b/src/components/MultiAccountManager.tsx @@ -0,0 +1,134 @@ +import React, { useState, useEffect } from 'react'; +import { Users, Server, Activity, ShieldCheck, Power, RefreshCw, Layers } from 'lucide-react'; +import { collection, onSnapshot, doc, updateDoc, query, orderBy } from 'firebase/firestore'; +import { db } from '../lib/firebase'; +import { cn } from '../lib/utils'; +import axios from 'axios'; + +export const MultiAccountManager = ({ userData }: { userData: any }) => { + const [clients, setClients] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const q = query(collection(db, 'users')); + const unsub = onSnapshot(q, (snapshot) => { + const list: any[] = []; + snapshot.forEach(doc => { + const user = doc.data(); + if (user.mtPlatform === 'deriv_api' || user.metaApiAccountId) { + list.push({ id: doc.id, ...user }); + } + }); + setClients(list); + setLoading(false); + }); + return () => unsub(); + }, []); + + const toggleClientEngine = async (clientId: string, currentState: boolean) => { + try { + await updateDoc(doc(db, 'users', clientId), { + 'tradingSettings.engineRunning': !currentState + }); + } catch (e: any) { + console.error("Failed to toggle engine", e); + } + }; + + if (loading) { + return ( +
+ +

Carregando Clientes...

+
+ ); + } + + return ( +
+
+
+
+ +
+
+

Multi Account Manager

+

+ Sistema Centralizado de Execução de Clientes +

+
+
+ +
+ {clients.length === 0 && ( +
+

Nenhum cliente com API configurada encontrado.

+
+ )} + {clients.map(client => { + const engineRunning = client.tradingSettings?.engineRunning ?? false; + const autoTrading = client.tradingSettings?.aiAutoTrading ?? false; + const platform = client.mtPlatform === 'deriv_api' ? 'Deriv API' : 'MetaTrader'; + const riskSettings = client.tradingSettings?.aiAggressiveness || 'Balanced'; + const takeProfit = client.tradingSettings?.takeProfit || 'N/A'; + + return ( +
+
+
+ {client.name || client.email} + {platform} • ID: {client.id.slice(0, 6)} +
+
+ {engineRunning ? "Online" : "Offline"} +
+
+ +
+
+
+ Confiança Mín. +
+
{client.tradingSettings?.aiMinConfidence || 85}% ({riskSettings})
+
+
+
+ Max Ordens +
+
{client.tradingSettings?.aiMaxOpenPositions || 2} Ordens
+
+
+ +
+
+ Status IA + {autoTrading ? ( + + ) : ( + + )} +
+ + +
+
+ ); + })} +
+
+
+ ); +}; diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 706dfaa..72fca04 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { LayoutDashboard, History, PieChart, Info, LogOut, CreditCard, ShieldCheck, User, Cpu } from 'lucide-react'; +import { LayoutDashboard, History, PieChart, Info, LogOut, CreditCard, ShieldCheck, User, Cpu, Users, Activity, Calendar as CalendarIcon } from 'lucide-react'; import { cn } from '../lib/utils'; interface NavbarProps { @@ -12,7 +12,9 @@ interface NavbarProps { export const Navbar: React.FC = ({ activeTab, setActiveTab, isAdmin, user }) => { const mainTabs = [ { id: 'scan', label: 'Scan IA', icon: LayoutDashboard }, + { id: 'autoScanner', label: 'Auto Scanner', icon: Activity }, { id: 'autoTrade', label: 'Auto Trading', icon: Cpu }, + { id: 'calendar', label: 'Calendário', icon: CalendarIcon }, { id: 'history', label: 'Histórico', icon: History }, { id: 'stats', label: 'Estatísticas', icon: PieChart }, ]; @@ -22,6 +24,7 @@ export const Navbar: React.FC = ({ activeTab, setActiveTab, isAdmin ]; if (isAdmin) { + accountTabs.push({ id: 'multiAccount', label: 'Multi Account', icon: Users }); accountTabs.push({ id: 'admin', label: 'Admin', icon: ShieldCheck }); } diff --git a/src/components/ProfileView.tsx b/src/components/ProfileView.tsx index f956526..3e754a8 100644 --- a/src/components/ProfileView.tsx +++ b/src/components/ProfileView.tsx @@ -26,6 +26,16 @@ export const ProfileView: React.FC<{ const [autoTradingEnabled, setAutoTradingEnabled] = useState(userData?.autoTradingEnabled || false); const [riskPerTrade, setRiskPerTrade] = useState(userData?.riskPerTrade || '2'); const [maxPositionsPerSignal, setMaxPositionsPerSignal] = useState(userData?.maxPositionsPerSignal || '1'); + + // Notification Config + const [notifTelegram, setNotifTelegram] = useState(userData?.notifications?.telegram || false); + const [notifPush, setNotifPush] = useState(userData?.notifications?.push || false); + const [notifEmail, setNotifEmail] = useState(userData?.notifications?.email || true); + const [notifyNewSignal, setNotifyNewSignal] = useState(userData?.notifications?.onNewSignal || true); + const [notifyTP, setNotifyTP] = useState(userData?.notifications?.onTP || true); + const [notifySL, setNotifySL] = useState(userData?.notifications?.onSL || true); + const [notifyOpen, setNotifyOpen] = useState(userData?.notifications?.onTradeOpen || false); + const [notifyClose, setNotifyClose] = useState(userData?.notifications?.onTradeClose || false); const handleUpdate = async (e: React.FormEvent) => { e.preventDefault(); @@ -48,6 +58,15 @@ export const ProfileView: React.FC<{ docUpdates.autoTradingEnabled = autoTradingEnabled; docUpdates.riskPerTrade = riskPerTrade; docUpdates.maxPositionsPerSignal = maxPositionsPerSignal; + + docUpdates['notifications.telegram'] = notifTelegram; + docUpdates['notifications.push'] = notifPush; + docUpdates['notifications.email'] = notifEmail; + docUpdates['notifications.onNewSignal'] = notifyNewSignal; + docUpdates['notifications.onTP'] = notifyTP; + docUpdates['notifications.onSL'] = notifySL; + docUpdates['notifications.onTradeOpen'] = notifyOpen; + docUpdates['notifications.onTradeClose'] = notifyClose; await updateDoc(doc(db, 'users', auth.currentUser.uid), docUpdates); onUpdate({ ...userData, ...docUpdates }); @@ -223,6 +242,57 @@ export const ProfileView: React.FC<{
+ + +
+

+ Central de Notificações +

+
+
+

Canais de Envio

+
+ + + +
+
+
+

Eventos Alertados

+
+ + + + + +
+
+
+ {/* WhatsApp Support Group */} diff --git a/src/components/SignalHistory.tsx b/src/components/SignalHistory.tsx index be2e6f7..79ff888 100644 --- a/src/components/SignalHistory.tsx +++ b/src/components/SignalHistory.tsx @@ -13,6 +13,7 @@ import axios from 'axios'; export const SignalHistory: React.FC = () => { const [signals, setSignals] = useState([]); const [loading, setLoading] = useState(true); + const [filterPreset, setFilterPreset] = useState<'hoje'|'semana'|'mes'|'personalizado'>('personalizado'); const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); const [expandedSignalIds, setExpandedSignalIds] = useState([]); @@ -246,6 +247,30 @@ export const SignalHistory: React.FC = () => { return "Fechado"; }; + const applyFilterPreset = (preset: 'hoje'|'semana'|'mes'|'personalizado') => { + setFilterPreset(preset); + const today = new Date(); + today.setHours(0,0,0,0); + + if (preset === 'hoje') { + const dStr = today.toISOString().split('T')[0]; + setStartDate(dStr); + setEndDate(dStr); + } else if (preset === 'semana') { + const startOfWeek = new Date(today); + startOfWeek.setDate(today.getDate() - today.getDay()); + setStartDate(startOfWeek.toISOString().split('T')[0]); + setEndDate(new Date().toISOString().split('T')[0]); + } else if (preset === 'mes') { + const startOfMonth = new Date(today.getFullYear(), today.getMonth(), 1); + setStartDate(startOfMonth.toISOString().split('T')[0]); + setEndDate(new Date().toISOString().split('T')[0]); + } else { + setStartDate(''); + setEndDate(''); + } + }; + return (
@@ -254,29 +279,37 @@ export const SignalHistory: React.FC = () => { Histórico de Sinais
-
- setStartDate(e.target.value)} - className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]" - /> - - - setEndDate(e.target.value)} - className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]" - /> - {(startDate || endDate) && ( - - )} +
+ + + +
+ {filterPreset === 'personalizado' && ( +
+ setStartDate(e.target.value)} + className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]" + /> + - + setEndDate(e.target.value)} + className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]" + /> + {(startDate || endDate) && ( + + )} +
+ )} {sortedSignals.length} Sinais diff --git a/src/components/TradingChart.tsx b/src/components/TradingChart.tsx index f436033..f290bad 100644 --- a/src/components/TradingChart.tsx +++ b/src/components/TradingChart.tsx @@ -90,7 +90,7 @@ export const TradingChart: React.FC = ({ signal }) => { seriesRef.current = mainSeries; } } catch (e) { - console.error("Failed to load chart", e); + console.error("Failed to load chart", e?.message || e); } finally { if (active) setLoading(false); } diff --git a/src/controllers/deriv.controller.ts b/src/controllers/deriv.controller.ts index b60af69..8cdcab2 100644 --- a/src/controllers/deriv.controller.ts +++ b/src/controllers/deriv.controller.ts @@ -54,7 +54,7 @@ export const connect = async (req: Request, res: Response) => { res.json({ success: true, message: 'Connected to Deriv' }); } catch (err: any) { - console.error('[Deriv] Connection Error:', err); + console.error('[Deriv] Connection Error:', err?.message || err); const errMessage = err?.message || String(err); res.status(500).json({ error: errMessage || 'Connection failed' }); } diff --git a/src/index.css b/src/index.css index a6e39d7..abbb5ed 100644 --- a/src/index.css +++ b/src/index.css @@ -45,7 +45,7 @@ } .glass-card { - @apply bg-brand-gray/15 backdrop-blur-md border border-white/5 rounded-xl p-4 md:p-5; + @apply bg-[#050505]/95 backdrop-blur-xl border border-white/[0.08] shadow-[0_8px_32px_rgba(0,0,0,0.8)] rounded-xl p-4 md:p-5; } .red-glow { diff --git a/src/services/marketData.ts b/src/services/marketData.ts index 4fd8c65..18eb2f3 100644 --- a/src/services/marketData.ts +++ b/src/services/marketData.ts @@ -124,7 +124,7 @@ export const checkHistoricalSignalResult = async (signal: Signal): Promise ({ @@ -316,7 +316,7 @@ export const fetchCurrentPrice = async (symbol: string): Promise } try { - const response = await axios.get(`/api/twelve/quote?symbol=${reqSymbol}`); + const response = await axios.get(`/api/twelve/quote?symbol=${encodeURIComponent(reqSymbol)}`); if (response.data && response.data.close) { const price = parseFloat(response.data.close); if (!isNaN(price)) return price; diff --git a/src/services/notifications.ts b/src/services/notifications.ts index ffe97f7..92a9e39 100644 --- a/src/services/notifications.ts +++ b/src/services/notifications.ts @@ -54,7 +54,7 @@ _Analisado por Inteligência Institucional_ console.error('Failed to send Telegram message', await response.text()); } - } catch (error) { - console.error('Error sending Telegram alert:', error); + } catch (error: any) { + console.error('Error sending Telegram alert:', error?.message || error); } } diff --git a/test_fmp.ts b/test_fmp.ts new file mode 100644 index 0000000..c74841d --- /dev/null +++ b/test_fmp.ts @@ -0,0 +1,4 @@ +import axios from 'axios'; +axios.get("https://financialmodelingprep.com/api/v3/economic_calendar?from=2024-01-01&to=2024-01-10&apikey=e3143522be4c4aac89e1f9a39925ed80") +.then(r => console.log('FMP success', r.status, Object.keys(r.data[0] || {}))) +.catch(e => console.log('FMP error', e.response?.status));