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.
This commit is contained in:
desartstudio95
2026-06-13 22:47:55 -07:00
parent 2d6e1a0809
commit 75db8e2ec0
20 changed files with 1383 additions and 517 deletions
+17 -4
View File
@@ -47,12 +47,13 @@ async function getCurrentPriceProxy(symbol: string): Promise<number | null> {
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({
+22 -11
View File
@@ -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<User | null>(null);
const [userData, setUserData] = useState<any>(null);
@@ -607,17 +611,18 @@ export default function App() {
<TourGuide hasCompletedTour={userData?.hasCompletedTour} />
<Toaster theme="dark" position="top-right" toastOptions={{ style: { background: '#0a0a0a', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' } }} />
<NotificationManager />
{/* Background Image for App */}
<div className="absolute inset-0 z-0 pointer-events-none">
<img
src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png"
alt="App Background"
className={cn(
"w-full h-full object-cover transition-opacity duration-500",
activeTab === 'history' ? "opacity-85" : "opacity-80"
)}
{/* Background Component for Institutional Look */}
<div className="fixed inset-0 z-0 pointer-events-none bg-black">
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top,_var(--tw-gradient-stops))] from-zinc-900/40 via-brand-dark to-brand-dark" />
<div
className="absolute inset-0 opacity-[0.03]"
style={{
backgroundImage: 'linear-gradient(to right, #ffffff 1px, transparent 1px), linear-gradient(to bottom, #ffffff 1px, transparent 1px)',
backgroundSize: '24px 24px',
backgroundPosition: 'center center'
}}
/>
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark/40 via-brand-dark/20 to-brand-dark" />
<div className="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-transparent via-brand-red/[0.015] to-transparent animate-[marquee_20s_linear_infinite]" style={{ backgroundSize: '100% 200%' }} />
</div>
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} user={user} />
@@ -631,10 +636,13 @@ 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 === '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'}
</h2>
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">
@@ -702,10 +710,13 @@ export default function App() {
transition={{ duration: 0.25, ease: 'easeOut' }}
>
{activeTab === 'scan' && <AnalysisView userData={userData} onGoToHistory={() => setActiveTab('history')} />}
{activeTab === 'autoScanner' && <AutoScanner247 userData={userData} />}
{activeTab === 'autoTrade' && <AutoTradingView userData={userData} onUpdate={setUserData} onNavigate={setActiveTab} isAdmin={isAdmin} />}
{activeTab === 'calendar' && <EconomicCalendar />}
{activeTab === 'history' && <SignalHistory />}
{activeTab === 'stats' && <DashboardStats />}
{activeTab === 'stats' && <DashboardStats userData={userData} />}
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
{activeTab === 'multiAccount' && isAdmin && <MultiAccountManager userData={userData} />}
{activeTab === 'admin' && isAdmin && <AdminDashboard />}
</motion.div>
</AnimatePresence>
+350 -29
View File
@@ -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<boolean>(false);
const lastSignalRef = useRef<number>(0);
const monitoringTradesRef = useRef<number[]>([]);
const highestROILogs = useRef<Record<number, number>>({});
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 (
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-5 space-y-4 shadow-[0_0_20px_rgba(255,0,0,0.05)]">
@@ -256,6 +463,114 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
</div>
</h3>
{/* AI Trading Settings Panel */}
<div className="bg-black/40 border border-white/5 rounded-xl p-4 space-y-3">
<div className="flex items-center justify-between mb-2 pb-2 border-b border-white/5">
<span className="text-xs font-black uppercase tracking-wider text-white">AI Trading Settings</span>
<label className="flex items-center gap-2 cursor-pointer">
<span className="text-[10px] font-bold text-zinc-400 uppercase tracking-widest">{aiAutoTrading ? 'Auto On' : 'Auto Off'}</span>
<div className={cn("w-8 h-4 rounded-full transition-colors relative", aiAutoTrading ? "bg-brand-red" : "bg-zinc-700")}>
<div className={cn("absolute top-0.5 bottom-0.5 w-3 rounded-full bg-white transition-all shadow", aiAutoTrading ? "left-4.5" : "left-0.5")}/>
</div>
<input type="checkbox" className="hidden" checked={aiAutoTrading} onChange={e => {
const val = e.target.checked;
setAiAutoTrading(val);
if (userData?.uid) {
updateDoc(doc(db, 'users', userData.uid), {
'tradingSettings.aiAutoTrading': val
}).then(() => setLastLog({ message: "Auto Trading " + (val ? "Ativado" : "Desativado"), isError: false, timestamp: Date.now() })).catch((err: any) => console.error(err?.message || err));
}
}} />
</label>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Max Open Positions</label>
<input
type="number"
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors"
value={aiMaxOpenPositions}
onChange={(e) => setAiMaxOpenPositions(e.target.value)}
onBlur={saveAISettings}
/>
</div>
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Trade Amount (USD)</label>
<input
type="number"
step="1"
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors"
value={aiTradeAmount}
onChange={(e) => setAiTradeAmount(e.target.value)}
onBlur={saveAISettings}
/>
</div>
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest flex items-center gap-1">Modo Copy Trading <span className="w-1.5 h-1.5 bg-brand-red rounded-full animate-pulse ml-1"></span></label>
<select
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors appearance-none"
value={aiAggressiveness}
onChange={(e) => handleAggressivenessChange(e.target.value)}
>
<option value="Conservador">Conservador (Risco Baixo)</option>
<option value="Moderado">Moderado (Risco Médio)</option>
<option value="Agressivo">Agressivo (Risco Alto)</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Cooldown (Sec)</label>
<input
type="number"
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors"
value={aiCooldownSeconds}
onChange={(e) => setAiCooldownSeconds(e.target.value)}
onBlur={saveAISettings}
/>
</div>
</div>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3">
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Risco por Operação (%)</label>
<select
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors appearance-none"
value={riskPerTrade}
onChange={(e) => setRiskPerTrade(e.target.value)}
onBlur={saveAISettings}
>
<option value="0.5">0.5%</option>
<option value="1">1%</option>
<option value="2">2%</option>
<option value="3">3%</option>
<option value="5">5%</option>
</select>
</div>
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Perda Máxima Diária ($)</label>
<input
type="number"
step="1"
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors"
value={dailyLossLimit}
onChange={(e) => setDailyLossLimit(e.target.value)}
onBlur={saveAISettings}
/>
</div>
<div className="space-y-1">
<label className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest">Meta Diária ($)</label>
<input
type="number"
step="1"
className="w-full bg-black border border-white/10 rounded-lg px-3 py-2 text-xs font-mono text-white focus:border-brand-red outline-none transition-colors"
value={dailyTargetProfit}
onChange={(e) => setDailyTargetProfit(e.target.value)}
onBlur={saveAISettings}
/>
</div>
</div>
</div>
{/* Performance Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
@@ -372,7 +687,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
<div className="mb-2">
<div className="text-zinc-600 uppercase tracking-widest text-[9px]">Payload:</div>
<pre className="text-zinc-400 bg-black/50 p-2 rounded mt-1 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(log.payload, null, 2)}
{(() => {
try {
return JSON.stringify(log.payload, null, 2);
} catch (e) {
return "[Circular or Unstringifiable Payload]";
}
})()}
</pre>
</div>
)}
+130 -139
View File
@@ -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
>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Score & Signal Info */}
<div className="glass-card flex flex-col items-center justify-center text-center space-y-4">
<div className="relative w-36 h-36 flex items-center justify-center">
<svg className="w-full h-full" viewBox="0 0 100 100">
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="rgba(255,255,255,0.03)"
strokeWidth="6"
/>
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="currentColor"
strokeWidth="6"
strokeDasharray={283}
strokeDashoffset={283 - (283 * result.score) / 100}
className={cn(
"transition-all duration-1000 ease-out",
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red" : "text-zinc-500")
)}
strokeLinecap="round"
transform="rotate(-90 50 50)"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<div className="glass-card flex flex-col space-y-4 relative overflow-hidden bg-gradient-to-b from-brand-gray/40 to-transparent">
<div className="absolute top-0 right-0 w-32 h-32 bg-brand-red/5 blur-3xl" />
<div className="flex flex-col md:flex-row items-center gap-6">
<div className="relative w-36 h-36 shrink-0 flex items-center justify-center">
<svg className="w-full h-full drop-shadow-[0_0_15px_rgba(255,0,0,0.1)]" viewBox="0 0 100 100">
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="rgba(255,255,255,0.03)"
strokeWidth="2"
/>
<circle
cx="50" cy="50" r="45"
fill="none"
stroke="currentColor"
strokeWidth="4"
strokeDasharray={283}
strokeDashoffset={283 - (283 * result.score) / 100}
className={cn(
"transition-all duration-1000 ease-out",
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red" : "text-zinc-500")
)}
strokeLinecap="round"
transform="rotate(-90 50 50)"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-[8px] font-black uppercase text-zinc-500 tracking-widest mb-0.5">IA SCORE</span>
<span className={cn(
"text-4xl font-black leading-none font-mono tracking-tighter",
result.decision === SignalType.BUY ? "text-green-500 text-shadow-sm shadow-green-500/50" : (result.decision === SignalType.SELL ? "text-brand-red red-text-glow" : "text-zinc-500")
)}>
{result.score}%
</span>
</div>
</div>
<div className="flex-1 space-y-3 z-10 w-full text-center md:text-left">
<div>
<h3 className={cn(
"text-xs font-black uppercase tracking-widest",
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'}
</h3>
<p className="text-zinc-400 mt-1.5 text-xs font-medium leading-relaxed border-l-2 border-white/10 pl-3 md:pl-0 md:border-l-0 text-left md:text-justify">{result.justification}</p>
<p className="text-brand-red mt-2 text-[10px] font-bold uppercase tracking-widest bg-brand-red/10 inline-block px-2 py-1 rounded border border-brand-red/20"> {result.alerta}</p>
</div>
</div>
</div>
<div className="grid grid-cols-3 gap-2 w-full pt-4 border-t border-white/5">
<div className="bg-black/40 border border-white/5 rounded p-2 text-center">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-1 tracking-widest">ATIVO IDENTIFICADO</span>
<span className="text-sm font-black text-white">{result.pair}</span>
</div>
<div className="bg-black/40 border border-white/5 rounded p-2 text-center">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-1 tracking-widest">TIMEFRAME</span>
<span className="text-sm font-black text-white">{result.timeframe}</span>
</div>
<div className="bg-black/40 border border-white/5 rounded p-2 text-center">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-1 tracking-widest">RISCO</span>
<span className={cn(
"text-4xl font-black leading-none",
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red red-text-glow" : "text-zinc-500")
)}>
{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'}</span>
</div>
</div>
<div className="bg-brand-red/5 border border-brand-red/10 rounded-lg p-3">
<h4 className="text-[9px] uppercase font-black tracking-widest text-brand-red mb-2 flex items-center gap-2">
<ScanLine size={12} /> Padrões & Estrutura Detectada
</h4>
<p className="text-xs text-brand-light font-medium leading-relaxed font-mono mt-1">
{result.estrutura || result.tecnica || 'Padrões de liquidez identificados pelo modelo Quântico.'}
</p>
</div>
</div>
{/* Trading Decision Block */}
<div className="glass-card space-y-4 bg-gradient-to-b from-black to-brand-gray/20">
<div className="flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex flex-col">
<span className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">{result.signalType || 'EXECUÇÃO'}</span>
<span className="text-sm font-black text-white">{result.pair}</span>
</div>
<div className={cn(
"px-4 py-1.5 rounded flex items-center justify-center border",
result.decision === SignalType.BUY ? "bg-green-500/10 text-green-500 border-green-500/20" :
result.decision === SignalType.SELL ? "bg-brand-red/10 text-brand-red border-brand-red/20" :
"bg-zinc-500/10 text-zinc-500 border-zinc-500/20"
)}>
<span className="text-xl font-black uppercase italic tracking-tighter">
{result.decision}
</span>
</div>
</div>
<div>
<h3 className={cn(
"text-sm font-black uppercase tracking-wider italic",
result.score >= 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'}
</h3>
<p className="text-zinc-500 mt-1 text-[10px] font-medium max-w-[200px]">{result.justification}</p>
<p className="text-brand-red mt-1 text-[10px] font-bold max-w-[200px] italic"> {result.alerta}</p>
</div>
<div className="w-full pt-4 border-t border-white/5 flex gap-3">
<div className="flex-1 glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">MODO</span>
<span className="text-xs font-black">{result.mode}</span>
</div>
<div className="flex-1 glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">TF</span>
<span className="text-xs font-black">{result.timeframe}</span>
</div>
</div>
{(result.riskReward || result.riskLevel || result.duration || result.signalType) && (
<div className="w-full pt-4 border-t border-white/5 grid grid-cols-2 gap-3">
{result.signalType && (
<div className="glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">ESTILO</span>
<span className="text-[10px] font-black tracking-widest">{result.signalType}</span>
</div>
)}
{result.riskLevel && (
<div className="glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">RISCO</span>
<span className={cn(
"text-[10px] font-black tracking-widest",
result.riskLevel.toUpperCase() === 'LOW' ? "text-green-500" :
result.riskLevel.toUpperCase() === 'HIGH' ? "text-brand-red" : "text-orange-500"
)}>{result.riskLevel}</span>
</div>
)}
{result.riskReward && (
<div className="glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">RISCO/RETORNO</span>
<span className="text-[10px] font-black font-mono">{result.riskReward}</span>
</div>
)}
{result.duration && (
<div className="glass-card p-2">
<span className="block text-[8px] text-zinc-600 uppercase font-black mb-0.5">DURAÇÃO</span>
<span className="text-[10px] font-black tracking-widest uppercase">{result.duration}</span>
</div>
)}
</div>
)}
</div>
{/* Trading Decision */}
<div className="glass-card space-y-4">
<div className="text-center">
<span className="text-[10px] font-black text-zinc-500 uppercase tracking-widest bg-white/5 px-2 py-0.5 rounded">
{result.pair}
</span>
</div>
<div className={cn(
"w-full py-5 rounded-lg flex flex-col items-center justify-center gap-1",
result.decision === SignalType.BUY ? "bg-green-500/10 text-green-500" :
result.decision === SignalType.SELL ? "bg-brand-red/10 text-brand-red" :
"bg-zinc-500/10 text-zinc-500"
)}>
<span className="text-4xl font-black uppercase italic tracking-tighter">
{result.decision === SignalType.BUY ? 'COMPRAR' :
result.decision === SignalType.SELL ? 'VENDER' :
'AGUARDAR'}
</span>
<span className="text-[8px] uppercase font-black tracking-widest opacity-60">Decisão QuantScan</span>
</div>
{marketPrice !== null && (
<div className="flex flex-col items-center justify-center p-2 mb-2 bg-black/40 rounded border border-white/5">
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold">Preço de Mercado</span>
<div className="flex items-center justify-between bg-black/60 p-3 rounded border border-white/5">
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold flex items-center gap-2"><Activity size={12}/> MARKET PRICE</span>
<span className="font-mono text-lg font-black text-white">{marketPrice.toFixed(5)}</span>
</div>
)}
<div className="space-y-2">
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="grid grid-cols-1 gap-2">
<div className="flex items-center justify-between p-2 bg-black/40 rounded border border-white/5 hover:border-white/10 transition-colors">
<div className="flex items-center gap-2">
<Target size={14} className="text-zinc-600" />
<span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span>
<Target size={14} className="text-blue-500" />
<span className="text-[9px] font-black text-zinc-400 uppercase tracking-widest">ENTRY LEVEL</span>
</div>
<div className="flex items-center gap-2">
<input
className="font-mono font-black text-sm bg-transparent border-b border-dashed border-white/20 text-right w-24 outline-none focus:border-white transition-colors text-white"
className="font-mono font-bold text-sm bg-transparent border-b border-dashed border-white/20 text-right w-24 outline-none focus:border-white transition-colors text-white"
value={customAlertPrices['Entrada'] ?? result.entry}
onChange={(e) => handleCustomAlertPriceChange('Entrada', e.target.value)}
/>
@@ -610,14 +599,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</button>
</div>
</div>
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center justify-between p-2 bg-brand-red/5 rounded border border-brand-red/10">
<div className="flex items-center gap-2">
<AlertTriangle size={14} className="text-brand-red/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span>
<AlertTriangle size={14} className="text-brand-red" />
<span className="text-[9px] font-black text-brand-red uppercase tracking-widest">STOP LOSS</span>
</div>
<div className="flex items-center gap-2">
<input
className="font-mono font-black text-sm bg-transparent border-b border-dashed border-brand-red/30 text-right w-24 outline-none focus:border-brand-red transition-colors text-brand-red"
className="font-mono font-bold text-sm bg-transparent border-b border-dashed border-brand-red/30 text-right w-24 outline-none focus:border-brand-red transition-colors text-brand-red"
value={customAlertPrices['Stop Loss'] ?? result.stopLoss}
onChange={(e) => handleCustomAlertPriceChange('Stop Loss', e.target.value)}
/>
@@ -632,14 +622,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</button>
</div>
</div>
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center justify-between p-2 bg-green-500/5 rounded border border-green-500/10">
<div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-green-500/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 1</span>
<ShieldCheck size={14} className="text-green-500" />
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 1</span>
</div>
<div className="flex items-center gap-2">
<input
className="font-mono font-black text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
className="font-mono font-bold text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
value={customAlertPrices['Take Profit 1'] ?? result.takeProfit}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 1', e.target.value)}
/>
@@ -656,14 +647,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</div>
{result.takeProfit2 && (
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center justify-between p-2 bg-green-500/5 rounded border border-green-500/10">
<div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-green-500/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 2</span>
<ShieldCheck size={14} className="text-green-500" />
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 2</span>
</div>
<div className="flex items-center gap-2">
<input
className="font-mono font-black text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
className="font-mono font-bold text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
value={customAlertPrices['Take Profit 2'] ?? result.takeProfit2}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 2', e.target.value)}
/>
@@ -681,14 +672,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
)}
{result.takeProfit3 && (
<div className="flex items-center justify-between p-3 glass-card bg-transparent border-white/5">
<div className="flex items-center justify-between p-2 bg-green-500/5 rounded border border-green-500/10">
<div className="flex items-center gap-2">
<ShieldCheck size={14} className="text-green-500/50" />
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 3</span>
<ShieldCheck size={14} className="text-green-500" />
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 3</span>
</div>
<div className="flex items-center gap-2">
<input
className="font-mono font-black text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
className="font-mono font-bold text-sm bg-transparent border-b border-dashed border-green-500/30 text-right w-24 outline-none focus:border-green-500 transition-colors text-green-500"
value={customAlertPrices['Take Profit 3'] ?? result.takeProfit3}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 3', e.target.value)}
/>
@@ -750,16 +741,16 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-brand-red flex items-center gap-2">
<BrainCircuit size={14} /> ANÁLISE TÉCNICA (SMC + LIQ)
<BrainCircuit size={14} /> RECONHECIMENTO DE PADRÕES & ESTRUTURA
</h4>
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
{result.tecnica}
{result.estrutura || result.tecnica}
</p>
</div>
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-green-500 flex items-center gap-2">
<TrendingUp size={14} /> ANÁLISE FUNDAMENTAL
<TrendingUp size={14} /> ANÁLISE FUNDAMENTAL & LIQUIDEZ
</h4>
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
{result.fundamental}
+255
View File
@@ -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<string, 'Bullish' | 'Bearish' | 'Neutral'>;
overall: 'BUY' | 'SELL' | 'WAIT';
confidence: number;
lastUpdated: number;
}
export const AutoScanner247 = ({ userData }: { userData: any }) => {
const [analyses, setAnalyses] = useState<AssetAnalysis[]>([]);
const [isScanning, setIsScanning] = useState(false);
const generateMockAnalysis = async (pair: string, type: string): Promise<AssetAnalysis> => {
const metrics: Record<string, 'Bullish' | 'Bearish' | 'Neutral'> = {};
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 (
<div className="space-y-6">
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-6 shadow-[0_0_20px_rgba(255,0,0,0.05)]">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center gap-3">
<div className="p-3 bg-brand-red/10 rounded-xl">
<Activity size={24} className="text-brand-red animate-pulse" />
</div>
<div>
<h2 className="text-xl font-black text-white uppercase tracking-wider flex items-center gap-2">
AUTO SCANNER 24/7
{isScanning && <RefreshCw size={14} className="animate-spin text-brand-red" />}
</h2>
<p className="text-xs text-zinc-500 font-medium uppercase tracking-widest mt-1">
Análise Quantitativa em Tempo Real
</p>
</div>
</div>
<div className="text-[10px] font-black uppercase tracking-widest bg-brand-red/20 text-brand-red px-3 py-1.5 rounded-full border border-brand-red/30 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-brand-red shadow-[0_0_8px_rgba(255,0,0,0.8)] animate-pulse" />
SYSTEM ONLINE
</div>
</div>
{/* Heatmap Section */}
<div className="mb-10">
<h3 className="text-sm font-black text-white uppercase tracking-widest mb-4 flex items-center gap-2 border-b border-white/5 pb-2">
<BarChart2 size={16} className="text-brand-red" />
Heatmap Inteligente
</h3>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-3">
{analyses.map(analysis => {
const category = getHeatmapCategory(analysis);
return (
<div key={`heat-${analysis.pair}`} className={cn("rounded-xl p-4 border flex flex-col items-center justify-center text-center transition-all", getHeatmapColor(category))}>
<span className="text-lg font-black tracking-widest mb-1 text-white">{analysis.pair}</span>
<span className="text-[9px] font-bold uppercase tracking-widest opacity-90 mb-3">{category}</span>
<div className="flex bg-black/40 rounded px-2 py-1 items-center gap-2 border border-white/5">
<span className="text-[10px] font-black uppercase tracking-widest">{analysis.overall}</span>
<span className="text-xs font-mono">{analysis.confidence}%</span>
</div>
</div>
);
})}
</div>
</div>
<div className="mb-4">
<h3 className="text-sm font-black text-white uppercase tracking-widest flex items-center gap-2 border-b border-white/5 pb-2">
<Zap size={16} className="text-brand-red" />
Análise Detalhada
</h3>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{analyses.map((analysis) => (
<div key={analysis.pair} className="bg-black/60 border border-white/5 rounded-xl p-5 hover:border-brand-red/30 transition-colors">
<div className="flex items-center justify-between border-b border-white/5 pb-3 mb-3">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<span className="text-lg font-black text-white tracking-widest">{analysis.pair}</span>
<span className={cn("text-[8px] font-bold uppercase tracking-widest px-2 py-0.5 rounded border", getTypeColor(analysis.type))}>
{analysis.type}
</span>
</div>
<span className="text-xs font-mono text-zinc-400">{analysis.price}</span>
</div>
<div className={cn(
"px-3 py-1 rounded-md text-[10px] font-black uppercase tracking-widest border",
analysis.overall === 'BUY' ? "bg-green-500/10 border-green-500/30 text-green-500" :
analysis.overall === 'SELL' ? "bg-brand-red/10 border-brand-red/30 text-brand-red" :
"bg-zinc-500/10 border-zinc-500/30 text-zinc-500"
)}>
{analysis.overall}
</div>
</div>
<div className="space-y-2 mb-4">
{ANALYSIS_CRITERIA.map(criteria => (
<div key={criteria} className="flex justify-between items-center text-[10px] font-bold uppercase tracking-widest border-b border-white/5 last:border-0 pb-1 last:pb-0">
<span className="text-zinc-500">{criteria}</span>
<span className={cn(
analysis.metrics[criteria] === 'Bullish' ? "text-green-500" :
analysis.metrics[criteria] === 'Bearish' ? "text-brand-red" :
"text-zinc-400"
)}>
{analysis.metrics[criteria]}
</span>
</div>
))}
</div>
<div className="flex items-center justify-between pt-3 border-t border-white/5">
<div className="text-[10px] uppercase font-black tracking-widest text-zinc-600">
Score Institucional:
</div>
<div className="flex items-center gap-2">
<div className="w-24 h-1.5 bg-zinc-900 rounded-full overflow-hidden">
<div
className={cn(
"h-full rounded-full transition-all duration-1000",
analysis.confidence >= 70 ? "bg-green-500" :
analysis.confidence >= 41 ? "bg-yellow-500" : "bg-brand-red"
)}
style={{ width: `${analysis.confidence}%` }}
/>
</div>
<span className={cn(
"text-xs font-mono font-black",
analysis.confidence >= 70 ? "text-green-500" :
analysis.confidence >= 41 ? "text-yellow-500" : "text-brand-red"
)}>{analysis.confidence}%</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
);
};
+81 -20
View File
@@ -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)
<>
<AITradingDashboard userData={userData} engineRunning={robotActive} />
<DerivDiagnosticPanel userData={userData} />
<DerivTestPanel userData={userData} />
</>
)}
@@ -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"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-green-500 uppercase tracking-widest">Take Profit (%)</label>
<input
type="number"
value={takeProfit}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-red-500 uppercase tracking-widest">Stop Loss (%)</label>
<input
type="number"
value={stopLoss}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Auto Close (mins)</label>
<input
type="number"
value={autoCloseMinutes}
onChange={(e) => 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"
/>
</div>
<div className="space-y-1 flex items-center justify-between col-span-2 pt-2 border-t border-white/5">
<span className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Trailing Stop</span>
<button
onClick={() => {
const newVal = !trailingStop;
setTrailingStop(newVal);
saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions); // Assuming state update is caught on re-render, though ideally we pass values directly if we could
}}
className={cn(
"relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus:outline-none",
trailingStop ? "bg-brand-red" : "bg-zinc-800"
)}
>
<span
className={cn(
"inline-block h-3 w-3 transform rounded-full bg-white transition-transform",
trailingStop ? "translate-x-5" : "translate-x-1"
)}
/>
</button>
</div>
</div>
</div>
</div>
@@ -986,23 +1043,27 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
{autoTrades.length === 0 ? (
<p className="text-xs text-zinc-500 text-center py-4">Nenhum trade executado ainda.</p>
) : (
autoTrades.map((t) => (
<div key={t.id} className="bg-black border border-white/5 p-4 rounded-xl flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className={cn("text-xs font-black uppercase tracking-wider", t.decision === 'BUY' ? 'text-green-500' : 'text-red-500')}>{t.decision}</span>
<span className="text-white font-bold text-sm">{t.pair}</span>
</div>
<div className="text-[10px] text-zinc-500 mt-1">
Vol: {t.volume} Price: {t.price?.toFixed(5)} SL: {t.stopLoss} TP: {t.takeProfit}
</div>
</div>
<div className="text-right">
<div className="text-[10px] text-zinc-400">{new Date(t.timestamp).toLocaleTimeString()}</div>
<div className="text-[10px] text-green-500 font-bold mt-1">EXECUTADO</div>
</div>
</div>
))
autoTrades.map((t) => (
<div key={t.id} className="bg-black border border-white/5 p-4 rounded-xl flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<span className={cn("text-xs font-black uppercase tracking-wider", (t.profit || 0) >= 0 ? 'text-green-500' : 'text-red-500')}>
{(t.profit || 0) >= 0 ? 'PROFIT' : 'LOSS'}
</span>
<span className="text-white font-bold text-sm">{t.symbol || t.pair || 'TRADE'}</span>
</div>
<div className="text-[10px] text-zinc-500 mt-1 uppercase">
ID: {t.contract_id || t.id} {t.close_reason ? `${t.close_reason}` : ''}
</div>
</div>
<div className="text-right">
<div className="text-[10px] text-zinc-400">{new Date(t.timestamp).toLocaleTimeString()}</div>
<div className={cn("text-xs font-bold mt-1 font-mono", (t.profit || 0) >= 0 ? "text-green-500" : "text-red-500")}>
{(t.profit || 0) >= 0 ? '+' : ''}{t.profit?.toFixed(2)} USD
</div>
</div>
</div>
))
)}
</div>
</div>
+105 -9
View File
@@ -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<Signal[]>([]);
const [loading, setLoading] = useState(true);
const [selectedPair, setSelectedPair] = useState<string>('');
const [instStats, setInstStats] = useState<InstitutionalStats>({ 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 = () => {
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
<header>
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
<Trophy size={20} className="text-brand-red" />
Leaderboard & Performance Global
<Wallet size={20} className="text-brand-red" />
Dashboard Institucional
</h1>
<p className="text-zinc-500 mt-1 text-[10px] font-medium leading-none">Acompanhamento e transparência audível dos resultados do sistema.</p>
</header>
{/* Institutional Core Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-4">
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-5 shadow-[0_0_15px_rgba(255,0,0,0.05)] relative overflow-hidden">
<div className="absolute -top-10 -right-10 w-24 h-24 bg-brand-red/10 blur-3xl rounded-full" />
<p className="text-[9px] text-zinc-500 uppercase tracking-widest font-black mb-1">Saldo Total</p>
<p className="text-2xl font-black text-white font-mono">${instStats.balance.toFixed(2)}</p>
</div>
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-5 relative overflow-hidden">
<p className="text-[9px] text-zinc-500 uppercase tracking-widest font-black mb-1">Equity</p>
<p className="text-2xl font-black text-white font-mono">${instStats.equity.toFixed(2)}</p>
</div>
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-5 relative overflow-hidden">
<p className="text-[9px] text-zinc-500 uppercase tracking-widest font-black mb-1">Lucro Hoje</p>
<p className={cn("text-2xl font-black font-mono", profitToday >= 0 ? "text-green-500" : "text-brand-red")}>
{profitToday >= 0 ? '+' : ''}{profitToday.toFixed(2)}
</p>
</div>
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-5 relative overflow-hidden">
<p className="text-[9px] text-zinc-500 uppercase tracking-widest font-black mb-1">Lucro Semana</p>
<p className={cn("text-2xl font-black font-mono", profitWeek >= 0 ? "text-green-500" : "text-brand-red")}>
{profitWeek >= 0 ? '+' : ''}{profitWeek.toFixed(2)}
</p>
</div>
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-5 relative overflow-hidden">
<p className="text-[9px] text-zinc-500 uppercase tracking-widest font-black mb-1">Lucro Mês</p>
<p className={cn("text-2xl font-black font-mono", profitMonthObj >= 0 ? "text-green-500" : "text-brand-red")}>
{profitMonthObj >= 0 ? '+' : ''}{profitMonthObj.toFixed(2)}
</p>
</div>
</div>
<motion.div
initial="hidden"
@@ -153,9 +249,9 @@ export const DashboardStats: React.FC = () => {
{[
{ 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) => (
<motion.div
@@ -168,7 +264,7 @@ export const DashboardStats: React.FC = () => {
>
<stat.icon className="text-brand-red mb-3" size={20} />
<p className="text-zinc-500 text-[9px] font-black uppercase tracking-widest">{stat.label}</p>
<p className="text-2xl font-black mt-0.5 text-white">{stat.value}</p>
<p className="text-2xl font-black mt-0.5 text-white truncate">{stat.value}</p>
</motion.div>
))}
</motion.div>
-272
View File
@@ -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<Position[]>([]);
const [logs, setLogs] = useState<LogEntry[]>([]);
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 (
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-5 space-y-6 mt-4">
<h3 className="font-black italic uppercase text-white tracking-widest text-lg border-b border-white/5 pb-3">Trading Test</h3>
{/* Controls */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-xs text-zinc-500 uppercase tracking-widest">Symbol</label>
<input
type="text"
value={symbol}
onChange={(e) => 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"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-500 uppercase tracking-widest">Amount (USD)</label>
<input
type="number"
value={amount}
min={1}
onChange={(e) => 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"
/>
</div>
<div className="space-y-2 flex flex-col justify-end">
<button
onClick={() => handleTrade('BUY')}
disabled={loading}
className="w-full bg-green-500/10 hover:bg-green-500/20 text-green-500 border border-green-500/30 rounded-lg px-3 py-2 font-black italic uppercase tracking-widest text-sm flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
>
<TrendingUp size={16} /> Buy Test
</button>
</div>
<div className="space-y-2 flex flex-col justify-end">
<button
onClick={() => handleTrade('SELL')}
disabled={loading}
className="w-full bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/30 rounded-lg px-3 py-2 font-black italic uppercase tracking-widest text-sm flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
>
<TrendingDown size={16} /> Sell Test
</button>
</div>
</div>
{/* Actions & Tables */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm text-zinc-400 font-bold uppercase tracking-widest">Open Positions</h4>
<div className="flex gap-2">
<button onClick={fetchPositions} disabled={loading} className="p-2 hover:bg-white/5 rounded-lg text-zinc-400 hover:text-white transition-colors">
<RefreshCcw size={16} className={loading ? 'animate-spin' : ''} />
</button>
<button onClick={closeAllPositions} disabled={loading || positions.length === 0} className="px-3 py-1.5 bg-brand-red/20 text-brand-red border border-brand-red/30 rounded-lg text-xs font-bold uppercase tracking-widest hover:bg-brand-red/30 transition-colors disabled:opacity-50">
Close All
</button>
</div>
</div>
<div className="bg-black/50 border border-white/5 rounded-xl overflow-hidden">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-white/5 text-zinc-500 text-[10px] uppercase tracking-widest">
<tr>
<th className="px-4 py-3 font-medium">Contract ID</th>
<th className="px-4 py-3 font-medium">Symbol</th>
<th className="px-4 py-3 font-medium">Direction</th>
<th className="px-4 py-3 font-medium">Entry Price</th>
<th className="px-4 py-3 font-medium text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 text-zinc-300">
{positions.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-zinc-500 text-xs uppercase tracking-widest">No open positions</td>
</tr>
) : (
positions.map((pos) => (
<tr key={pos.contract_id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3 font-mono text-xs">{pos.contract_id}</td>
<td className="px-4 py-3 font-bold">{pos.symbol}</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest ${pos.contract_type.includes('UP') || pos.contract_type === 'CALL' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
{pos.contract_type}
</span>
</td>
<td className="px-4 py-3 font-mono">{pos.buy_price} {pos.currency}</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => closePosition(pos.contract_id)}
className="text-brand-red hover:text-red-400 transition-colors p-1"
title="Close Position"
>
<XCircle size={16} />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Logs */}
<div className="space-y-4">
<h4 className="text-sm text-zinc-400 font-bold uppercase tracking-widest">Trade Logs</h4>
<div className="bg-black/50 border border-white/5 rounded-xl overflow-hidden max-h-60 overflow-y-auto">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-white/5 text-zinc-500 text-[10px] uppercase tracking-widest sticky top-0">
<tr>
<th className="px-4 py-3 font-medium">Time</th>
<th className="px-4 py-3 font-medium">Action</th>
<th className="px-4 py-3 font-medium">Result</th>
<th className="px-4 py-3 font-medium">Error</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 text-zinc-300">
{logs.length === 0 ? (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-zinc-500 text-xs uppercase tracking-widest">No logs yet</td>
</tr>
) : (
logs.map((log, i) => (
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-2 font-mono text-[10px] text-zinc-500 w-32 flex items-center gap-1.5">
<Clock size={12} />
{log.time.toLocaleTimeString()}
</td>
<td className="px-4 py-2 font-bold text-xs"><span className="px-2 py-0.5 bg-white/5 rounded border border-white/10">{log.action}</span></td>
<td className="px-4 py-2 text-xs flex items-center gap-1.5">
{log.error ? <AlertTriangle size={12} className="text-yellow-500" /> : <CheckCircle2 size={12} className="text-green-500" />}
{log.result}
</td>
<td className="px-4 py-2 text-xs text-red-400 w-full truncate max-w-xs" title={log.error}>{log.error || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
+146
View File
@@ -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<EventProps[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(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 (
<div className="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-700 h-full flex flex-col">
<header className="flex-shrink-0">
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
<CalendarIcon size={20} className="text-brand-red" />
Calendário Econômico
</h1>
<p className="text-zinc-500 mt-1 text-[10px] font-medium leading-none">Acompanhe eventos de alto impacto no mercado em tempo real.</p>
</header>
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-6 shadow-xl flex-1 overflow-hidden flex flex-col">
{loading ? (
<div className="flex-1 flex flex-col items-center justify-center text-zinc-500">
<Loader2 className="w-8 h-8 animate-spin text-brand-red mb-3" />
<span className="text-sm font-semibold tracking-wide uppercase">Sincronizando feed institucional...</span>
</div>
) : error ? (
<div className="flex-1 flex flex-col items-center justify-center text-brand-red">
<AlertCircle className="w-8 h-8 mb-3 opacity-80" />
<span className="text-sm font-medium">{error}</span>
</div>
) : (
<div className="overflow-x-auto h-full flex-1">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-white/10 text-[9px] uppercase tracking-widest text-zinc-500 font-black">
<th className="pb-3 px-4 font-bold">Date & Time</th>
<th className="pb-3 px-4 font-bold">Cur</th>
<th className="pb-3 px-4 font-bold">Impact</th>
<th className="pb-3 px-4 font-bold">Event</th>
<th className="pb-3 px-4 font-bold text-right">Actual</th>
<th className="pb-3 px-4 font-bold text-right">Forecast</th>
<th className="pb-3 px-4 font-bold text-right">Previous</th>
</tr>
</thead>
<tbody>
{events.map((event) => (
<tr key={event.id} className="border-b border-white/5 last:border-0 hover:bg-white/[0.02] transition-colors">
<td className="py-4 px-4 text-xs font-mono text-zinc-400 whitespace-nowrap">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2">
<CalendarIcon size={10} className="text-zinc-600" />
{new Date(event.date).toLocaleDateString()}
</div>
<div className="flex items-center gap-2">
<Clock size={10} className="text-zinc-600" />
{event.time}
</div>
</div>
</td>
<td className="py-4 px-4 text-xs font-black text-white">{event.currency}</td>
<td className="py-4 px-4">
<span className={cn("px-2 py-1 rounded text-[9px] font-black uppercase tracking-widest border", getImpactColor(event.impact))}>
{event.impact}
</span>
</td>
<td className="py-4 px-4 text-sm font-semibold text-white truncate max-w-[200px] md:max-w-none">{event.title}</td>
<td className="py-4 px-4 text-xs font-mono text-white text-right">{event.actual || '-'}</td>
<td className="py-4 px-4 text-xs font-mono text-zinc-400 text-right">{event.forecast || '-'}</td>
<td className="py-4 px-4 text-xs font-mono text-zinc-500 text-right">{event.previous || '-'}</td>
</tr>
))}
</tbody>
</table>
{events.length === 0 && (
<div className="text-center text-zinc-500 mt-10 text-xs">Nenhum evento económico restante esta semana.</div>
)}
</div>
)}
</div>
</div>
);
};
+2 -2
View File
@@ -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);
}
};
+134
View File
@@ -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<any[]>([]);
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 (
<div className="flex flex-col items-center justify-center py-20 text-brand-red">
<RefreshCw size={32} className="animate-spin mb-4" />
<p className="text-xs font-black uppercase tracking-widest text-white">Carregando Clientes...</p>
</div>
);
}
return (
<div className="space-y-6">
<div className="bg-[#0a0000] border border-white/5 rounded-2xl p-6">
<div className="flex items-center gap-3 mb-6">
<div className="p-3 bg-brand-red/10 rounded-xl">
<Users size={24} className="text-brand-red" />
</div>
<div>
<h2 className="text-xl font-black text-white uppercase tracking-wider">Multi Account Manager</h2>
<p className="text-xs text-zinc-500 font-medium uppercase tracking-widest mt-1">
Sistema Centralizado de Execução de Clientes
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{clients.length === 0 && (
<div className="col-span-1 lg:col-span-2 text-center py-10 bg-black/40 border border-white/5 rounded-xl">
<p className="text-sm font-bold text-zinc-500 uppercase tracking-widest">Nenhum cliente com API configurada encontrado.</p>
</div>
)}
{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 (
<div key={client.id} className="bg-black/60 border border-white/5 rounded-xl p-5 space-y-4">
<div className="flex items-start justify-between border-b border-white/5 pb-4">
<div className="flex flex-col">
<span className="text-sm font-black text-white truncate max-w-[200px]">{client.name || client.email}</span>
<span className="text-[10px] text-zinc-500 font-bold uppercase tracking-widest">{platform} &bull; ID: {client.id.slice(0, 6)}</span>
</div>
<div className={cn("px-2 py-1 rounded text-[9px] font-black uppercase tracking-widest border",
engineRunning ? "bg-green-500/10 text-green-500 border-green-500/20" : "bg-zinc-500/10 text-zinc-500 border-zinc-500/20"
)}>
{engineRunning ? "Online" : "Offline"}
</div>
</div>
<div className="grid grid-cols-2 gap-3 mb-4">
<div className="bg-black border border-white/5 rounded-lg p-3">
<div className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest mb-1 flex items-center gap-1">
<Activity size={10} /> Confiança Mín.
</div>
<div className="text-xs font-mono text-white">{client.tradingSettings?.aiMinConfidence || 85}% ({riskSettings})</div>
</div>
<div className="bg-black border border-white/5 rounded-lg p-3">
<div className="text-[9px] uppercase font-bold text-zinc-500 tracking-widest mb-1 flex items-center gap-1">
<Layers size={10} /> Max Ordens
</div>
<div className="text-xs font-mono text-white">{client.tradingSettings?.aiMaxOpenPositions || 2} Ordens</div>
</div>
</div>
<div className="flex items-center justify-between pt-2">
<div className="flex items-center gap-2">
<span className="text-[9px] font-black uppercase tracking-widest text-zinc-400">Status IA</span>
{autoTrading ? (
<span className="w-2 h-2 rounded-full bg-brand-red shadow-[0_0_8px_rgba(255,0,0,0.8)] animate-pulse" />
) : (
<span className="w-2 h-2 rounded-full bg-zinc-600" />
)}
</div>
<button
onClick={() => toggleClientEngine(client.id, engineRunning)}
className={cn(
"flex items-center gap-2 px-4 py-2 rounded-lg text-xs font-black uppercase tracking-widest transition-all",
engineRunning
? "bg-zinc-800 text-white hover:bg-zinc-700"
: "bg-brand-red text-white hover:bg-red-500"
)}
>
<Power size={14} />
{engineRunning ? 'Parar Motor' : 'Ativar Master'}
</button>
</div>
</div>
);
})}
</div>
</div>
</div>
);
};
+4 -1
View File
@@ -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<NavbarProps> = ({ 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<NavbarProps> = ({ activeTab, setActiveTab, isAdmin
];
if (isAdmin) {
accountTabs.push({ id: 'multiAccount', label: 'Multi Account', icon: Users });
accountTabs.push({ id: 'admin', label: 'Admin', icon: ShieldCheck });
}
+70
View File
@@ -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<{
</div>
</form>
</motion.div>
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.15 }} className="glass-card p-6 relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-brand-red/10 blur-3xl" />
<h3 className="text-sm font-black italic uppercase text-white tracking-widest mb-4 flex items-center gap-2">
Central de Notificações
</h3>
<div className="space-y-4">
<div>
<p className="text-[10px] uppercase font-black tracking-widest text-zinc-500 mb-2">Canais de Envio</p>
<div className="flex flex-wrap gap-4">
<label className="flex items-center gap-2 text-xs font-bold text-white cursor-pointer">
<input type="checkbox" checked={notifTelegram} onChange={(e) => { setNotifTelegram(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Telegram
</label>
<label className="flex items-center gap-2 text-xs font-bold text-white cursor-pointer">
<input type="checkbox" checked={notifPush} onChange={(e) => { setNotifPush(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Push Notification
</label>
<label className="flex items-center gap-2 text-xs font-bold text-white cursor-pointer">
<input type="checkbox" checked={notifEmail} onChange={(e) => { setNotifEmail(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Email
</label>
</div>
</div>
<div>
<p className="text-[10px] uppercase font-black tracking-widest text-zinc-500 mb-2">Eventos Alertados</p>
<div className="grid grid-cols-2 gap-3">
<label className="flex items-center gap-2 text-xs font-medium text-zinc-300 cursor-pointer">
<input type="checkbox" checked={notifyNewSignal} onChange={(e) => { setNotifyNewSignal(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Novo Sinal Gerado
</label>
<label className="flex items-center gap-2 text-xs font-medium text-zinc-300 cursor-pointer">
<input type="checkbox" checked={notifyTP} onChange={(e) => { setNotifyTP(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Take Profit (TP) Atingido
</label>
<label className="flex items-center gap-2 text-xs font-medium text-zinc-300 cursor-pointer">
<input type="checkbox" checked={notifySL} onChange={(e) => { setNotifySL(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Stop Loss (SL) Atingido
</label>
<label className="flex items-center gap-2 text-xs font-medium text-zinc-300 cursor-pointer">
<input type="checkbox" checked={notifyOpen} onChange={(e) => { setNotifyOpen(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Operação Aberta (Auto)
</label>
<label className="flex items-center gap-2 text-xs font-medium text-zinc-300 cursor-pointer">
<input type="checkbox" checked={notifyClose} onChange={(e) => { setNotifyClose(e.target.checked); handleUpdate(e as any); }} className="accent-brand-red" />
Operação Encerrada
</label>
</div>
</div>
</div>
</motion.div>
{/* WhatsApp Support Group */}
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="glass-card p-6 border-[#25D366]/30 bg-[#25D366]/5">
+55 -22
View File
@@ -13,6 +13,7 @@ import axios from 'axios';
export const SignalHistory: React.FC = () => {
const [signals, setSignals] = useState<Signal[]>([]);
const [loading, setLoading] = useState(true);
const [filterPreset, setFilterPreset] = useState<'hoje'|'semana'|'mes'|'personalizado'>('personalizado');
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = useState<string>('');
const [expandedSignalIds, setExpandedSignalIds] = useState<string[]>([]);
@@ -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 (
<div className="space-y-6">
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
@@ -254,29 +279,37 @@ export const SignalHistory: React.FC = () => {
Histórico de Sinais
</h1>
<div className="flex flex-col sm:flex-row gap-3 items-center">
<div className="flex flex-wrap sm:flex-nowrap bg-brand-gray/50 rounded-lg p-1 border border-white/5 items-center">
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]"
/>
<span className="text-zinc-600 px-2 py-1">-</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]"
/>
{(startDate || endDate) && (
<button
onClick={() => { setStartDate(''); setEndDate(''); }}
className="text-xs text-brand-red px-2 hover:bg-brand-red/10 rounded py-1 ml-1"
>
Limpar
</button>
)}
<div className="flex gap-2">
<button onClick={() => applyFilterPreset('hoje')} className={cn("px-3 py-1 rounded text-[10px] font-black uppercase tracking-widest transition-colors", filterPreset === 'hoje' ? "bg-brand-red text-white" : "bg-brand-gray text-zinc-500 hover:text-white")}>Hoje</button>
<button onClick={() => applyFilterPreset('semana')} className={cn("px-3 py-1 rounded text-[10px] font-black uppercase tracking-widest transition-colors", filterPreset === 'semana' ? "bg-brand-red text-white" : "bg-brand-gray text-zinc-500 hover:text-white")}>Semana</button>
<button onClick={() => applyFilterPreset('mes')} className={cn("px-3 py-1 rounded text-[10px] font-black uppercase tracking-widest transition-colors", filterPreset === 'mes' ? "bg-brand-red text-white" : "bg-brand-gray text-zinc-500 hover:text-white")}>Mês</button>
<button onClick={() => applyFilterPreset('personalizado')} className={cn("px-3 py-1 rounded text-[10px] font-black uppercase tracking-widest transition-colors", filterPreset === 'personalizado' ? "bg-brand-red text-white" : "bg-brand-gray text-zinc-500 hover:text-white")}>Pers.</button>
</div>
{filterPreset === 'personalizado' && (
<div className="flex flex-wrap sm:flex-nowrap bg-brand-gray/50 rounded-lg p-1 border border-white/5 items-center">
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]"
/>
<span className="text-zinc-600 px-2 py-1">-</span>
<input
type="date"
value={endDate}
onChange={(e) => setEndDate(e.target.value)}
className="bg-transparent text-sm text-zinc-300 outline-none px-2 py-1 [color-scheme:dark]"
/>
{(startDate || endDate) && (
<button
onClick={() => { setStartDate(''); setEndDate(''); }}
className="text-xs text-brand-red px-2 hover:bg-brand-red/10 rounded py-1 ml-1"
>
Limpar
</button>
)}
</div>
)}
<span className="bg-brand-gray px-4 py-2 rounded-full text-zinc-400 text-sm font-bold border border-white/5 whitespace-nowrap">
{sortedSignals.length} Sinais
</span>
+1 -1
View File
@@ -90,7 +90,7 @@ export const TradingChart: React.FC<TradingChartProps> = ({ 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);
}
+1 -1
View File
@@ -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' });
}
+1 -1
View File
@@ -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 {
+3 -3
View File
@@ -124,7 +124,7 @@ export const checkHistoricalSignalResult = async (signal: Signal): Promise<strin
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
const response = await axios.get(`/api/twelve/history?symbol=${encodeURIComponent(twelveSymbol)}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
// TwelveData returns newest first. We need to iterate from oldest (after signal start) to newest.
const values = [...response.data.values].reverse();
@@ -216,7 +216,7 @@ export const fetchChartData = async (symbol: string): Promise<{ time: number, op
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
const response = await axios.get(`/api/twelve/history?symbol=${encodeURIComponent(twelveSymbol)}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
const values = [...response.data.values].reverse();
return values.map((candle: any) => ({
@@ -316,7 +316,7 @@ export const fetchCurrentPrice = async (symbol: string): Promise<number | null>
}
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;
+2 -2
View File
@@ -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);
}
}
+4
View File
@@ -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));