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:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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} • 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>
|
||||
);
|
||||
};
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
Reference in New Issue
Block a user