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
|
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 {
|
try {
|
||||||
let twelveSymbol = symStr;
|
let twelveSymbol = symStr;
|
||||||
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/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);
|
if (res.data && res.data.price) return parseFloat(res.data.price);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
// Fallback silently if TwelveData is rate-limited or fails
|
// 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: [] });
|
return res.json({ values: [] });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process.env.TWELVE_DATA_API_KEY) {
|
const apiKey = process.env.TWELVE_DATA_API_KEY || "e3143522be4c4aac89e1f9a39925ed80";
|
||||||
|
if (apiKey) {
|
||||||
try {
|
try {
|
||||||
let twelveSymbol = symStr;
|
let twelveSymbol = symStr;
|
||||||
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/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) {
|
if (tkres.data && tkres.data.values) {
|
||||||
return res.json({ values: 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
|
// Vite middleware for development
|
||||||
if (process.env.NODE_ENV !== "production") {
|
if (process.env.NODE_ENV !== "production") {
|
||||||
const vite = await createViteServer({
|
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() {
|
export default function App() {
|
||||||
const [user, setUser] = useState<User | null>(null);
|
const [user, setUser] = useState<User | null>(null);
|
||||||
const [userData, setUserData] = useState<any>(null);
|
const [userData, setUserData] = useState<any>(null);
|
||||||
@@ -607,17 +611,18 @@ export default function App() {
|
|||||||
<TourGuide hasCompletedTour={userData?.hasCompletedTour} />
|
<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' } }} />
|
<Toaster theme="dark" position="top-right" toastOptions={{ style: { background: '#0a0a0a', border: '1px solid rgba(255,255,255,0.1)', color: '#fff' } }} />
|
||||||
<NotificationManager />
|
<NotificationManager />
|
||||||
{/* Background Image for App */}
|
{/* Background Component for Institutional Look */}
|
||||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
<div className="fixed inset-0 z-0 pointer-events-none bg-black">
|
||||||
<img
|
<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" />
|
||||||
src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png"
|
<div
|
||||||
alt="App Background"
|
className="absolute inset-0 opacity-[0.03]"
|
||||||
className={cn(
|
style={{
|
||||||
"w-full h-full object-cover transition-opacity duration-500",
|
backgroundImage: 'linear-gradient(to right, #ffffff 1px, transparent 1px), linear-gradient(to bottom, #ffffff 1px, transparent 1px)',
|
||||||
activeTab === 'history' ? "opacity-85" : "opacity-80"
|
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>
|
</div>
|
||||||
|
|
||||||
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} user={user} />
|
<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>
|
<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">
|
<h2 className="text-2xl font-black italic uppercase tracking-tighter text-white">
|
||||||
{activeTab === 'scan' && 'Scanner de IA'}
|
{activeTab === 'scan' && 'Scanner de IA'}
|
||||||
|
{activeTab === 'autoScanner' && 'Auto Scanner 24/7'}
|
||||||
{activeTab === 'autoTrade' && 'Auto Trading'}
|
{activeTab === 'autoTrade' && 'Auto Trading'}
|
||||||
|
{activeTab === 'calendar' && 'Calendário Econômico'}
|
||||||
{activeTab === 'history' && 'Histórico de Sinais'}
|
{activeTab === 'history' && 'Histórico de Sinais'}
|
||||||
{activeTab === 'stats' && 'Performance & Dados'}
|
{activeTab === 'stats' && 'Performance & Dados'}
|
||||||
{activeTab === 'profile' && 'Perfil do Usuário'}
|
{activeTab === 'profile' && 'Perfil do Usuário'}
|
||||||
|
{activeTab === 'multiAccount' && 'Multi Account Manager'}
|
||||||
{activeTab === 'admin' && 'Painel Administrativo'}
|
{activeTab === 'admin' && 'Painel Administrativo'}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-[0.2em] text-zinc-500">
|
<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' }}
|
transition={{ duration: 0.25, ease: 'easeOut' }}
|
||||||
>
|
>
|
||||||
{activeTab === 'scan' && <AnalysisView userData={userData} onGoToHistory={() => setActiveTab('history')} />}
|
{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 === 'autoTrade' && <AutoTradingView userData={userData} onUpdate={setUserData} onNavigate={setActiveTab} isAdmin={isAdmin} />}
|
||||||
|
{activeTab === 'calendar' && <EconomicCalendar />}
|
||||||
{activeTab === 'history' && <SignalHistory />}
|
{activeTab === 'history' && <SignalHistory />}
|
||||||
{activeTab === 'stats' && <DashboardStats />}
|
{activeTab === 'stats' && <DashboardStats userData={userData} />}
|
||||||
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
|
{activeTab === 'profile' && <ProfileView user={user} userData={userData} onUpdate={setUserData} onDeleted={handleLogout} />}
|
||||||
|
{activeTab === 'multiAccount' && isAdmin && <MultiAccountManager userData={userData} />}
|
||||||
{activeTab === 'admin' && isAdmin && <AdminDashboard />}
|
{activeTab === 'admin' && isAdmin && <AdminDashboard />}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { AITradingEngine, RiskManager, AISignal } from '../services/aiTradingEng
|
|||||||
import { Activity, Target, TrendingUp, AlertCircle, Clock, Zap, X } from 'lucide-react';
|
import { Activity, Target, TrendingUp, AlertCircle, Clock, Zap, X } from 'lucide-react';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { cn } from '../lib/utils';
|
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';
|
import { db } from '../lib/firebase';
|
||||||
|
|
||||||
export interface ErrorLogDetail {
|
export interface ErrorLogDetail {
|
||||||
@@ -39,11 +39,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
|
|
||||||
setErrorLogs(prev => [log, ...prev].slice(0, 100));
|
setErrorLogs(prev => [log, ...prev].slice(0, 100));
|
||||||
setLastLog({ message: `Erro: ${log.message}`, isError: true, timestamp: log.timestamp });
|
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({
|
const [stats, setStats] = useState({
|
||||||
winRate: 68.5,
|
winRate: 68.5,
|
||||||
|
totalTrades: 0,
|
||||||
|
winTrades: 0,
|
||||||
tradesToday: 0,
|
tradesToday: 0,
|
||||||
profitToday: 0,
|
profitToday: 0,
|
||||||
lastTradeAction: '-',
|
lastTradeAction: '-',
|
||||||
@@ -53,6 +55,122 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
const statsRef = useRef(stats);
|
const statsRef = useRef(stats);
|
||||||
useEffect(() => { statsRef.current = stats; }, [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 token = userData?.savedDerivToken || userData?.mtPassword;
|
||||||
const appId = userData?.derivAppId || '1089';
|
const appId = userData?.derivAppId || '1089';
|
||||||
const symbol = userData?.tradingSettings?.selectedAsset || 'R_100';
|
const symbol = userData?.tradingSettings?.selectedAsset || 'R_100';
|
||||||
@@ -60,15 +178,21 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
const activeCycleRef = useRef<boolean>(false);
|
const activeCycleRef = useRef<boolean>(false);
|
||||||
const lastSignalRef = useRef<number>(0);
|
const lastSignalRef = useRef<number>(0);
|
||||||
const monitoringTradesRef = useRef<number[]>([]);
|
const monitoringTradesRef = useRef<number[]>([]);
|
||||||
|
const highestROILogs = useRef<Record<number, number>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!engineRunning || userData?.mtPlatform !== 'deriv_api' || !token) return;
|
if (userData?.mtPlatform !== 'deriv_api' || !token) return;
|
||||||
|
|
||||||
const executeCycle = async () => {
|
const executeCycle = async () => {
|
||||||
if (activeCycleRef.current) return;
|
if (activeCycleRef.current) return;
|
||||||
activeCycleRef.current = true;
|
activeCycleRef.current = true;
|
||||||
|
|
||||||
try {
|
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
|
// 1. Monitor Phase
|
||||||
const positionsToKeep: number[] = [];
|
const positionsToKeep: number[] = [];
|
||||||
for (const contractId of monitoringTradesRef.current) {
|
for (const contractId of monitoringTradesRef.current) {
|
||||||
@@ -82,20 +206,55 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
|
|
||||||
if (contract.is_sold) {
|
if (contract.is_sold) {
|
||||||
setLastLog({ message: `Trade ${contractId} fechado externamente. Lucro: $${contract.profit.toFixed(2)}`, isError: false, timestamp: Date.now() });
|
setLastLog({ message: `Trade ${contractId} fechado externamente. Lucro: $${contract.profit.toFixed(2)}`, isError: false, timestamp: Date.now() });
|
||||||
setStats(prev => ({
|
setStats(prev => {
|
||||||
...prev,
|
const isWin = contract.profit >= 0;
|
||||||
profitToday: prev.profitToday + contract.profit,
|
const newTotal = prev.totalTrades + 1;
|
||||||
lastTradeResult: contract.profit >= 0 ? 'WIN' : 'LOSS'
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buyPrice = contract.buy_price;
|
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;
|
const ageSeconds = (Date.now() / 1000) - contract.date_start;
|
||||||
|
|
||||||
// Close if +/- 10% ROI, or after 60 seconds
|
// Update Highest ROI
|
||||||
if (roi >= 0.1 || roi <= -0.1 || ageSeconds > 60) {
|
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 {
|
try {
|
||||||
await axios.post(`/api/deriv/close-trade?appId=${appId}`, { contractId }, {
|
await axios.post(`/api/deriv/close-trade?appId=${appId}`, { contractId }, {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
@@ -106,19 +265,30 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
});
|
});
|
||||||
const finalProfit = finalRes.data?.contract?.profit || contract.profit;
|
const finalProfit = finalRes.data?.contract?.profit || contract.profit;
|
||||||
|
|
||||||
setLastLog({ message: `Trade ${contractId} finalizado. Lucro: $${finalProfit.toFixed(2)}`, isError: false, timestamp: Date.now() });
|
setLastLog({ message: `Trade ${contractId} finalizado. ${closeReason}. Lucro: $${finalProfit.toFixed(2)}`, isError: false, timestamp: Date.now() });
|
||||||
setStats(prev => ({
|
setStats(prev => {
|
||||||
...prev,
|
const isWin = finalProfit >= 0;
|
||||||
profitToday: prev.profitToday + finalProfit,
|
const newTotal = prev.totalTrades + 1;
|
||||||
lastTradeResult: finalProfit >= 0 ? 'WIN' : 'LOSS'
|
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) {
|
if (userData?.uid) {
|
||||||
await addDoc(collection(db, "users", userData.uid, "auto_trades"), {
|
await addDoc(collection(db, "users", userData.uid, "auto_trades"), {
|
||||||
contract_id: contractId,
|
contract_id: contractId,
|
||||||
profit: finalProfit,
|
profit: finalProfit,
|
||||||
timestamp: Date.now(),
|
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 => logDetailedError("Erro ao salvar trade no firestore", e, "firebase/addDoc"));
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
@@ -129,8 +299,13 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
positionsToKeep.push(contractId);
|
positionsToKeep.push(contractId);
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
const errMessage = e.response?.data?.error || e.message;
|
||||||
logDetailedError("Failed to fetch contract status", e, `/api/deriv/contract/${contractId}?appId=${appId}`);
|
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);
|
setLastSignal(signal);
|
||||||
|
|
||||||
const openPositions = monitoringTradesRef.current.length;
|
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 MIN_STAKE = 1;
|
||||||
const requestedAmount = amount;
|
const requestedAmount = amount;
|
||||||
if (amount < MIN_STAKE) {
|
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() });
|
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 = {
|
const riskLimits = {
|
||||||
maxDailyLoss: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 100,
|
maxDailyLoss: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 100,
|
||||||
maxOpenPositions: parseInt(userData?.tradingSettings?.maxPositions) || 3,
|
maxOpenPositions: !isNaN(engineMaxOpen) ? engineMaxOpen : 2,
|
||||||
maxTradeValue: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 50,
|
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;
|
const timeSinceLastTrade = Date.now() - statsRef.current.lastTradeTime;
|
||||||
|
|
||||||
if (statsRef.current.lastTradeTime > 0 && timeSinceLastTrade < cooldownMinutes * 60 * 1000) {
|
if (statsRef.current.lastTradeTime > 0 && timeSinceLastTrade < cooldownMs) {
|
||||||
setLastLog({ message: `Sinal ignorado (Cooldown): Aguarde ${Math.ceil((cooldownMinutes * 60 * 1000 - timeSinceLastTrade) / 1000)}s`, isError: true, timestamp: Date.now() });
|
setLastLog({ message: `Sinal ignorado (Cooldown): Aguarde ${Math.ceil((cooldownMs - timeSinceLastTrade) / 1000)}s`, isError: true, timestamp: Date.now() });
|
||||||
return; // skip execution
|
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) {
|
if (!validation.valid) {
|
||||||
setLastLog({ message: `Sinal rejeitado: ${validation.reason}`, isError: true, timestamp: Date.now() });
|
setLastLog({ message: `Sinal rejeitado: ${validation.reason}`, isError: true, timestamp: Date.now() });
|
||||||
@@ -232,7 +439,7 @@ export const AITradingDashboard = ({ userData, engineRunning }: { userData: any,
|
|||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
activeCycleRef.current = false;
|
activeCycleRef.current = false;
|
||||||
};
|
};
|
||||||
}, [engineRunning, token, appId, symbol, userData]);
|
}, [engineRunning, aiAutoTrading, aiMaxOpenPositions, aiTradeAmount, aiMinConfidence, aiCooldownSeconds, token, appId, symbol, userData]);
|
||||||
|
|
||||||
return (
|
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)]">
|
<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>
|
</div>
|
||||||
</h3>
|
</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 */}
|
{/* Performance Stats */}
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<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">
|
<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="mb-2">
|
||||||
<div className="text-zinc-600 uppercase tracking-widest text-[9px]">Payload:</div>
|
<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">
|
<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>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+130
-139
@@ -61,8 +61,8 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
currentPrice = price;
|
currentPrice = price;
|
||||||
checkAlerts(price);
|
checkAlerts(price);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error("Error fetching market price", e);
|
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', {
|
axios.post('/api/onesignal/notify', {
|
||||||
title: 'Novo Sinal QuantScan IA 🚨',
|
title: 'Novo Sinal QuantScan IA 🚨',
|
||||||
message: `${sanitizedAnalysis.pair} - ${sanitizedAnalysis.decision} (${sanitizedAnalysis.signalType || 'Scalping'}). Confiança: ${sanitizedAnalysis.score}%`
|
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) {
|
} 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.';
|
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')) {
|
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.';
|
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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
{/* Score & Signal Info */}
|
{/* Score & Signal Info */}
|
||||||
<div className="glass-card flex flex-col items-center justify-center text-center space-y-4">
|
<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="relative w-36 h-36 flex items-center justify-center">
|
<div className="absolute top-0 right-0 w-32 h-32 bg-brand-red/5 blur-3xl" />
|
||||||
<svg className="w-full h-full" viewBox="0 0 100 100">
|
|
||||||
<circle
|
<div className="flex flex-col md:flex-row items-center gap-6">
|
||||||
cx="50" cy="50" r="45"
|
<div className="relative w-36 h-36 shrink-0 flex items-center justify-center">
|
||||||
fill="none"
|
<svg className="w-full h-full drop-shadow-[0_0_15px_rgba(255,0,0,0.1)]" viewBox="0 0 100 100">
|
||||||
stroke="rgba(255,255,255,0.03)"
|
<circle
|
||||||
strokeWidth="6"
|
cx="50" cy="50" r="45"
|
||||||
/>
|
fill="none"
|
||||||
<circle
|
stroke="rgba(255,255,255,0.03)"
|
||||||
cx="50" cy="50" r="45"
|
strokeWidth="2"
|
||||||
fill="none"
|
/>
|
||||||
stroke="currentColor"
|
<circle
|
||||||
strokeWidth="6"
|
cx="50" cy="50" r="45"
|
||||||
strokeDasharray={283}
|
fill="none"
|
||||||
strokeDashoffset={283 - (283 * result.score) / 100}
|
stroke="currentColor"
|
||||||
className={cn(
|
strokeWidth="4"
|
||||||
"transition-all duration-1000 ease-out",
|
strokeDasharray={283}
|
||||||
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red" : "text-zinc-500")
|
strokeDashoffset={283 - (283 * result.score) / 100}
|
||||||
)}
|
className={cn(
|
||||||
strokeLinecap="round"
|
"transition-all duration-1000 ease-out",
|
||||||
transform="rotate(-90 50 50)"
|
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red" : "text-zinc-500")
|
||||||
/>
|
)}
|
||||||
</svg>
|
strokeLinecap="round"
|
||||||
<div className="absolute inset-0 flex flex-col items-center justify-center">
|
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(
|
<span className={cn(
|
||||||
"text-4xl font-black leading-none",
|
"text-sm font-black tracking-widest",
|
||||||
result.decision === SignalType.BUY ? "text-green-500" : (result.decision === SignalType.SELL ? "text-brand-red red-text-glow" : "text-zinc-500")
|
result.riskLevel?.toUpperCase() === 'LOW' ? "text-green-500" :
|
||||||
)}>
|
result.riskLevel?.toUpperCase() === 'HIGH' ? "text-brand-red" : "text-orange-500"
|
||||||
{result.score}%
|
)}>{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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{marketPrice !== null && (
|
||||||
<div className="flex flex-col items-center justify-center p-2 mb-2 bg-black/40 rounded border border-white/5">
|
<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">Preço de Mercado</span>
|
<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>
|
<span className="font-mono text-lg font-black text-white">{marketPrice.toFixed(5)}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
<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-black/40 rounded border border-white/5 hover:border-white/10 transition-colors">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Target size={14} className="text-zinc-600" />
|
<Target size={14} className="text-blue-500" />
|
||||||
<span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span>
|
<span className="text-[9px] font-black text-zinc-400 uppercase tracking-widest">ENTRY LEVEL</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<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}
|
value={customAlertPrices['Entrada'] ?? result.entry}
|
||||||
onChange={(e) => handleCustomAlertPriceChange('Entrada', e.target.value)}
|
onChange={(e) => handleCustomAlertPriceChange('Entrada', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -610,14 +599,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center gap-2">
|
||||||
<AlertTriangle size={14} className="text-brand-red/50" />
|
<AlertTriangle size={14} className="text-brand-red" />
|
||||||
<span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span>
|
<span className="text-[9px] font-black text-brand-red uppercase tracking-widest">STOP LOSS</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<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}
|
value={customAlertPrices['Stop Loss'] ?? result.stopLoss}
|
||||||
onChange={(e) => handleCustomAlertPriceChange('Stop Loss', e.target.value)}
|
onChange={(e) => handleCustomAlertPriceChange('Stop Loss', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -632,14 +622,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div className="flex items-center gap-2">
|
||||||
<ShieldCheck size={14} className="text-green-500/50" />
|
<ShieldCheck size={14} className="text-green-500" />
|
||||||
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 1</span>
|
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 1</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<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}
|
value={customAlertPrices['Take Profit 1'] ?? result.takeProfit}
|
||||||
onChange={(e) => handleCustomAlertPriceChange('Take Profit 1', e.target.value)}
|
onChange={(e) => handleCustomAlertPriceChange('Take Profit 1', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -656,14 +647,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{result.takeProfit2 && (
|
{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">
|
<div className="flex items-center gap-2">
|
||||||
<ShieldCheck size={14} className="text-green-500/50" />
|
<ShieldCheck size={14} className="text-green-500" />
|
||||||
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 2</span>
|
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 2</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<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}
|
value={customAlertPrices['Take Profit 2'] ?? result.takeProfit2}
|
||||||
onChange={(e) => handleCustomAlertPriceChange('Take Profit 2', e.target.value)}
|
onChange={(e) => handleCustomAlertPriceChange('Take Profit 2', e.target.value)}
|
||||||
/>
|
/>
|
||||||
@@ -681,14 +672,14 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{result.takeProfit3 && (
|
{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">
|
<div className="flex items-center gap-2">
|
||||||
<ShieldCheck size={14} className="text-green-500/50" />
|
<ShieldCheck size={14} className="text-green-500" />
|
||||||
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 3</span>
|
<span className="text-[9px] font-black text-green-500 uppercase tracking-widest">TAKE PROFIT 3</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<input
|
<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}
|
value={customAlertPrices['Take Profit 3'] ?? result.takeProfit3}
|
||||||
onChange={(e) => handleCustomAlertPriceChange('Take Profit 3', e.target.value)}
|
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">
|
<div className="glass-card space-y-4">
|
||||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-brand-red flex items-center gap-2">
|
<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>
|
</h4>
|
||||||
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
||||||
{result.tecnica}
|
{result.estrutura || result.tecnica}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="glass-card space-y-4">
|
<div className="glass-card space-y-4">
|
||||||
<h4 className="text-[10px] font-black uppercase tracking-widest text-green-500 flex items-center gap-2">
|
<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>
|
</h4>
|
||||||
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
<p className="text-xs text-zinc-400 leading-relaxed font-medium">
|
||||||
{result.fundamental}
|
{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
|
Fingerprint, ChevronRight, Server, Cpu, Radio, Network, CheckCircle2, ShieldCheck
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { DerivDiagnosticPanel } from './DerivDiagnosticPanel';
|
import { DerivDiagnosticPanel } from './DerivDiagnosticPanel';
|
||||||
import { DerivTestPanel } from './DerivTestPanel';
|
|
||||||
import { AITradingDashboard } from './AITradingDashboard';
|
import { AITradingDashboard } from './AITradingDashboard';
|
||||||
import { cn } from '../lib/utils';
|
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 [dailyLossLimit, setDailyLossLimit] = useState(userData?.tradingSettings?.dailyLossLimit?.toString() || "100");
|
||||||
const [dailyProfitTarget, setDailyProfitTarget] = useState(userData?.tradingSettings?.dailyProfitTarget?.toString() || "200");
|
const [dailyProfitTarget, setDailyProfitTarget] = useState(userData?.tradingSettings?.dailyProfitTarget?.toString() || "200");
|
||||||
const [cooldownMinutes, setCooldownMinutes] = useState(userData?.tradingSettings?.cooldownMinutes?.toString() || "5");
|
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 (
|
const saveSettingsToFirebase = async (
|
||||||
newSettings: any[],
|
newSettings: any[],
|
||||||
@@ -110,7 +113,11 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
|
|||||||
minConfidence: parseFloat(minConfidence) || 80,
|
minConfidence: parseFloat(minConfidence) || 80,
|
||||||
dailyLossLimit: parseFloat(dailyLossLimit) || 100,
|
dailyLossLimit: parseFloat(dailyLossLimit) || 100,
|
||||||
dailyProfitTarget: parseFloat(dailyProfitTarget) || 200,
|
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 {
|
try {
|
||||||
@@ -364,7 +371,6 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
|
|||||||
<>
|
<>
|
||||||
<AITradingDashboard userData={userData} engineRunning={robotActive} />
|
<AITradingDashboard userData={userData} engineRunning={robotActive} />
|
||||||
<DerivDiagnosticPanel userData={userData} />
|
<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"
|
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>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -986,23 +1043,27 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
|
|||||||
{autoTrades.length === 0 ? (
|
{autoTrades.length === 0 ? (
|
||||||
<p className="text-xs text-zinc-500 text-center py-4">Nenhum trade executado ainda.</p>
|
<p className="text-xs text-zinc-500 text-center py-4">Nenhum trade executado ainda.</p>
|
||||||
) : (
|
) : (
|
||||||
autoTrades.map((t) => (
|
autoTrades.map((t) => (
|
||||||
<div key={t.id} className="bg-black border border-white/5 p-4 rounded-xl flex items-center justify-between">
|
<div key={t.id} className="bg-black border border-white/5 p-4 rounded-xl flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<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={cn("text-xs font-black uppercase tracking-wider", (t.profit || 0) >= 0 ? 'text-green-500' : 'text-red-500')}>
|
||||||
<span className="text-white font-bold text-sm">{t.pair}</span>
|
{(t.profit || 0) >= 0 ? 'PROFIT' : 'LOSS'}
|
||||||
</div>
|
</span>
|
||||||
<div className="text-[10px] text-zinc-500 mt-1">
|
<span className="text-white font-bold text-sm">{t.symbol || t.pair || 'TRADE'}</span>
|
||||||
Vol: {t.volume} • Price: {t.price?.toFixed(5)} • SL: {t.stopLoss} • TP: {t.takeProfit}
|
</div>
|
||||||
</div>
|
<div className="text-[10px] text-zinc-500 mt-1 uppercase">
|
||||||
</div>
|
ID: {t.contract_id || t.id} {t.close_reason ? `• ${t.close_reason}` : ''}
|
||||||
<div className="text-right">
|
</div>
|
||||||
<div className="text-[10px] text-zinc-400">{new Date(t.timestamp).toLocaleTimeString()}</div>
|
</div>
|
||||||
<div className="text-[10px] text-green-500 font-bold mt-1">EXECUTADO</div>
|
<div className="text-right">
|
||||||
</div>
|
<div className="text-[10px] text-zinc-400">{new Date(t.timestamp).toLocaleTimeString()}</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,14 +6,24 @@ import {
|
|||||||
BarChart, Bar, Cell,
|
BarChart, Bar, Cell,
|
||||||
LineChart, Line, Legend
|
LineChart, Line, Legend
|
||||||
} from 'recharts';
|
} 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 { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore';
|
||||||
import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
|
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 [signals, setSignals] = useState<Signal[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedPair, setSelectedPair] = useState<string>('');
|
const [selectedPair, setSelectedPair] = useState<string>('');
|
||||||
|
const [instStats, setInstStats] = useState<InstitutionalStats>({ balance: 0, equity: 0, profitFactor: 0, expectancy: 0, drawdown: 0 });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!auth.currentUser) return;
|
if (!auth.currentUser) return;
|
||||||
@@ -38,6 +48,28 @@ export const DashboardStats: React.FC = () => {
|
|||||||
return () => unsubscribe();
|
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(() => {
|
useEffect(() => {
|
||||||
if (!selectedPair && signals.length > 0) {
|
if (!selectedPair && signals.length > 0) {
|
||||||
const pairs = Array.from(new Set(signals.filter(s => s.pair).map(s => s.pair)));
|
const pairs = Array.from(new Set(signals.filter(s => s.pair).map(s => s.pair)));
|
||||||
@@ -96,7 +128,10 @@ export const DashboardStats: React.FC = () => {
|
|||||||
.sort((a, b) => a.timestamp - b.timestamp)
|
.sort((a, b) => a.timestamp - b.timestamp)
|
||||||
.reduce((acc: any[], signal, index) => {
|
.reduce((acc: any[], signal, index) => {
|
||||||
const prevProfit = index > 0 ? acc[index - 1].profit : 0;
|
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({
|
acc.push({
|
||||||
name: new Date(signal.timestamp).toLocaleDateString(),
|
name: new Date(signal.timestamp).toLocaleDateString(),
|
||||||
profit: prevProfit + change
|
profit: prevProfit + change
|
||||||
@@ -104,6 +139,36 @@ export const DashboardStats: React.FC = () => {
|
|||||||
return acc;
|
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 timeframeStats = signals.reduce((acc: any[], signal) => {
|
||||||
const existing = acc.find(i => i.name === signal.timeframe);
|
const existing = acc.find(i => i.name === signal.timeframe);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -136,12 +201,43 @@ export const DashboardStats: React.FC = () => {
|
|||||||
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-700">
|
||||||
<header>
|
<header>
|
||||||
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
|
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
|
||||||
<Trophy size={20} className="text-brand-red" />
|
<Wallet size={20} className="text-brand-red" />
|
||||||
Leaderboard & Performance Global
|
Dashboard Institucional
|
||||||
</h1>
|
</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>
|
<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>
|
</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
|
<motion.div
|
||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate="visible"
|
animate="visible"
|
||||||
@@ -153,9 +249,9 @@ export const DashboardStats: React.FC = () => {
|
|||||||
{[
|
{[
|
||||||
{ label: 'Total Sinais', value: totalSignals, icon: Activity },
|
{ label: 'Total Sinais', value: totalSignals, icon: Activity },
|
||||||
{ label: 'Taxa de Acerto', value: `${winRate.toFixed(1)}%`, icon: Award },
|
{ label: 'Taxa de Acerto', value: `${winRate.toFixed(1)}%`, icon: Award },
|
||||||
{ label: 'Win Rate Mensal', value: `${monthlyWinRate.toFixed(1)}%`, icon: Calendar },
|
{ label: 'Expectancy', value: `$${expectancy.toFixed(2)}`, icon: Calendar },
|
||||||
{ label: 'Série de Vitórias', value: `${maxStreak} Seguida${maxStreak !== 1 ? 's' : ''}`, icon: Flame },
|
{ label: 'Drawdown Máx', value: `$${maxDrawdown.toFixed(2)}`, icon: ChevronDown },
|
||||||
{ label: 'Ratio Win/Loss', value: profitLossRatio, icon: Target },
|
{ label: 'Profit Factor', value: profitFactor.toFixed(2), icon: Target },
|
||||||
{ label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp },
|
{ label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp },
|
||||||
].map((stat, i) => (
|
].map((stat, i) => (
|
||||||
<motion.div
|
<motion.div
|
||||||
@@ -168,7 +264,7 @@ export const DashboardStats: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<stat.icon className="text-brand-red mb-3" size={20} />
|
<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-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>
|
||||||
))}
|
))}
|
||||||
</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);
|
setQuotes(newQuotes);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
} catch (e) {
|
} catch (e: any) {
|
||||||
console.error("Failed to fetch market data", e);
|
console.error("Failed to fetch market data", e?.message || e);
|
||||||
setLoading(false);
|
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 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';
|
import { cn } from '../lib/utils';
|
||||||
|
|
||||||
interface NavbarProps {
|
interface NavbarProps {
|
||||||
@@ -12,7 +12,9 @@ interface NavbarProps {
|
|||||||
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin, user }) => {
|
export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin, user }) => {
|
||||||
const mainTabs = [
|
const mainTabs = [
|
||||||
{ id: 'scan', label: 'Scan IA', icon: LayoutDashboard },
|
{ id: 'scan', label: 'Scan IA', icon: LayoutDashboard },
|
||||||
|
{ id: 'autoScanner', label: 'Auto Scanner', icon: Activity },
|
||||||
{ id: 'autoTrade', label: 'Auto Trading', icon: Cpu },
|
{ id: 'autoTrade', label: 'Auto Trading', icon: Cpu },
|
||||||
|
{ id: 'calendar', label: 'Calendário', icon: CalendarIcon },
|
||||||
{ id: 'history', label: 'Histórico', icon: History },
|
{ id: 'history', label: 'Histórico', icon: History },
|
||||||
{ id: 'stats', label: 'Estatísticas', icon: PieChart },
|
{ id: 'stats', label: 'Estatísticas', icon: PieChart },
|
||||||
];
|
];
|
||||||
@@ -22,6 +24,7 @@ export const Navbar: React.FC<NavbarProps> = ({ activeTab, setActiveTab, isAdmin
|
|||||||
];
|
];
|
||||||
|
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
|
accountTabs.push({ id: 'multiAccount', label: 'Multi Account', icon: Users });
|
||||||
accountTabs.push({ id: 'admin', label: 'Admin', icon: ShieldCheck });
|
accountTabs.push({ id: 'admin', label: 'Admin', icon: ShieldCheck });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,16 @@ export const ProfileView: React.FC<{
|
|||||||
const [riskPerTrade, setRiskPerTrade] = useState(userData?.riskPerTrade || '2');
|
const [riskPerTrade, setRiskPerTrade] = useState(userData?.riskPerTrade || '2');
|
||||||
const [maxPositionsPerSignal, setMaxPositionsPerSignal] = useState(userData?.maxPositionsPerSignal || '1');
|
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) => {
|
const handleUpdate = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!auth.currentUser) return;
|
if (!auth.currentUser) return;
|
||||||
@@ -49,6 +59,15 @@ export const ProfileView: React.FC<{
|
|||||||
docUpdates.riskPerTrade = riskPerTrade;
|
docUpdates.riskPerTrade = riskPerTrade;
|
||||||
docUpdates.maxPositionsPerSignal = maxPositionsPerSignal;
|
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);
|
await updateDoc(doc(db, 'users', auth.currentUser.uid), docUpdates);
|
||||||
onUpdate({ ...userData, ...docUpdates });
|
onUpdate({ ...userData, ...docUpdates });
|
||||||
|
|
||||||
@@ -224,6 +243,57 @@ export const ProfileView: React.FC<{
|
|||||||
</form>
|
</form>
|
||||||
</motion.div>
|
</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 */}
|
{/* 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">
|
<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">
|
||||||
<h3 className="text-sm font-black italic uppercase text-white tracking-widest mb-2 flex items-center gap-2">
|
<h3 className="text-sm font-black italic uppercase text-white tracking-widest mb-2 flex items-center gap-2">
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import axios from 'axios';
|
|||||||
export const SignalHistory: React.FC = () => {
|
export const SignalHistory: React.FC = () => {
|
||||||
const [signals, setSignals] = useState<Signal[]>([]);
|
const [signals, setSignals] = useState<Signal[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [filterPreset, setFilterPreset] = useState<'hoje'|'semana'|'mes'|'personalizado'>('personalizado');
|
||||||
const [startDate, setStartDate] = useState<string>('');
|
const [startDate, setStartDate] = useState<string>('');
|
||||||
const [endDate, setEndDate] = useState<string>('');
|
const [endDate, setEndDate] = useState<string>('');
|
||||||
const [expandedSignalIds, setExpandedSignalIds] = useState<string[]>([]);
|
const [expandedSignalIds, setExpandedSignalIds] = useState<string[]>([]);
|
||||||
@@ -246,6 +247,30 @@ export const SignalHistory: React.FC = () => {
|
|||||||
return "Fechado";
|
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 (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<header className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
<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
|
Histórico de Sinais
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col sm:flex-row gap-3 items-center">
|
<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">
|
<div className="flex gap-2">
|
||||||
<input
|
<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>
|
||||||
type="date"
|
<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>
|
||||||
value={startDate}
|
<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>
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
<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>
|
||||||
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>
|
</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">
|
<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
|
{sortedSignals.length} Sinais
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export const TradingChart: React.FC<TradingChartProps> = ({ signal }) => {
|
|||||||
seriesRef.current = mainSeries;
|
seriesRef.current = mainSeries;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Failed to load chart", e);
|
console.error("Failed to load chart", e?.message || e);
|
||||||
} finally {
|
} finally {
|
||||||
if (active) setLoading(false);
|
if (active) setLoading(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const connect = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
res.json({ success: true, message: 'Connected to Deriv' });
|
res.json({ success: true, message: 'Connected to Deriv' });
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[Deriv] Connection Error:', err);
|
console.error('[Deriv] Connection Error:', err?.message || err);
|
||||||
const errMessage = err?.message || String(err);
|
const errMessage = err?.message || String(err);
|
||||||
res.status(500).json({ error: errMessage || 'Connection failed' });
|
res.status(500).json({ error: errMessage || 'Connection failed' });
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -45,7 +45,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.glass-card {
|
.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 {
|
.red-glow {
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export const checkHistoricalSignalResult = async (signal: Signal): Promise<strin
|
|||||||
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/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)) {
|
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.
|
// TwelveData returns newest first. We need to iterate from oldest (after signal start) to newest.
|
||||||
const values = [...response.data.values].reverse();
|
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 === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
|
||||||
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/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)) {
|
if (response.data && response.data.values && Array.isArray(response.data.values)) {
|
||||||
const values = [...response.data.values].reverse();
|
const values = [...response.data.values].reverse();
|
||||||
return values.map((candle: any) => ({
|
return values.map((candle: any) => ({
|
||||||
@@ -316,7 +316,7 @@ export const fetchCurrentPrice = async (symbol: string): Promise<number | null>
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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) {
|
if (response.data && response.data.close) {
|
||||||
const price = parseFloat(response.data.close);
|
const price = parseFloat(response.data.close);
|
||||||
if (!isNaN(price)) return price;
|
if (!isNaN(price)) return price;
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ _Analisado por Inteligência Institucional_
|
|||||||
console.error('Failed to send Telegram message', await response.text());
|
console.error('Failed to send Telegram message', await response.text());
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
console.error('Error sending Telegram alert:', error);
|
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