feat: Integrate OneSignal and add AI strategy features

Adds OneSignal for push notifications and new AI strategy parameters like trailing stop, winrate learning, and multi-time frame analysis. Also updates build scripts and dependencies.
This commit is contained in:
desartstudio95
2026-05-20 03:26:54 -07:00
parent a579152114
commit babce35231
21 changed files with 2795 additions and 237 deletions
+10
View File
@@ -7,3 +7,13 @@ GEMINI_API_KEY="MY_GEMINI_API_KEY"
# AI Studio automatically injects this at runtime with the Cloud Run service URL. # AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints. # Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL" APP_URL="MY_APP_URL"
# Twelve Data API
TWELVE_DATA_API_KEY=
# OneSignal Frontend Config
VITE_ONESIGNAL_APP_ID=
# OneSignal Backend Config
ONESIGNAL_APP_ID=
ONESIGNAL_REST_API_KEY=
+1 -1
View File
@@ -1,5 +1,5 @@
<div align="center"> <div align="center">
<img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" /> <img width="1200" height="475" alt="GHBanner" src="https://ai.google.dev/static/site-assets/images/share-ais-513315318.png" />
</div> </div>
# Run and deploy your AI Studio app # Run and deploy your AI Studio app
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title> <title>My Google AI Studio App</title>
<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
+1189 -113
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -4,8 +4,9 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --port=3000 --host=0.0.0.0", "dev": "tsx server.ts",
"build": "vite build", "build": "vite build && esbuild server.ts --bundle --platform=node --format=cjs --packages=external --sourcemap --outfile=dist/server.cjs",
"start": "node dist/server.cjs",
"preview": "vite preview", "preview": "vite preview",
"clean": "rm -rf dist", "clean": "rm -rf dist",
"lint": "tsc --noEmit" "lint": "tsc --noEmit"
@@ -16,6 +17,7 @@
"@google/genai": "^1.29.0", "@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"axios": "^1.16.1",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@@ -23,11 +25,14 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"express": "^4.21.2", "express": "^4.21.2",
"firebase": "^12.12.1", "firebase": "^12.12.1",
"lightweight-charts": "^5.2.0",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"onesignal-node": "^3.4.0",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-dropzone": "^15.0.0", "react-dropzone": "^15.0.0",
"react-onesignal": "^3.5.2",
"recharts": "^3.8.1", "recharts": "^3.8.1",
"shadcn": "^4.7.0", "shadcn": "^4.7.0",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.5.0",
@@ -39,6 +44,7 @@
"@types/express": "^4.17.21", "@types/express": "^4.17.21",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"esbuild": "^0.28.0",
"eslint": "^10.2.1", "eslint": "^10.2.1",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.1.14",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
+103
View File
@@ -0,0 +1,103 @@
import express from "express";
import path from "path";
import axios from "axios";
import { createServer as createViteServer } from "vite";
import * as OneSignal from 'onesignal-node';
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json());
// API Routes
app.get("/api/health", (req, res) => {
res.json({ status: "ok" });
});
// Twelve Data endpoint
app.get("/api/twelve/quote", async (req, res) => {
const symbol = req.query.symbol || "EUR/USD";
const apiKey = process.env.TWELVE_DATA_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "TWELVE_DATA_API_KEY is not configured" });
}
try {
const response = await axios.get(`https://api.twelvedata.com/quote?symbol=${symbol}&apikey=${apiKey}`);
res.json(response.data);
} catch (error) {
console.error("Error fetching Twelve Data", error);
res.status(500).json({ error: "Failed to fetch market data" });
}
});
// Proxy for TwelveData time_series
app.get("/api/twelve/history", async (req, res) => {
const symbol = req.query.symbol as string;
const apiKey = process.env.TWELVE_DATA_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "TWELVE_DATA_API_KEY is not configured" });
}
try {
const response = await axios.get(`https://api.twelvedata.com/time_series?symbol=${symbol}&interval=15min&outputsize=500&apikey=${apiKey}`);
res.json(response.data);
} catch (error) {
console.error("Error fetching Twelve Data History", error);
res.status(500).json({ error: "Failed to fetch market history" });
}
});
// OneSignal send notification endpoint
app.post("/api/onesignal/notify", async (req, res) => {
const { title, message } = req.body;
const appId = process.env.ONESIGNAL_APP_ID;
const restApiKey = process.env.ONESIGNAL_REST_API_KEY;
if (!appId || !restApiKey) {
return res.status(500).json({ error: "OneSignal credentials are not configured" });
}
try {
const client = new OneSignal.Client(appId, restApiKey);
const notification = {
contents: {
'en': message || 'New Analysis Available',
},
headings: {
'en': title || 'QuantScan IA Update',
},
included_segments: ['Subscribed Users'],
};
const response = await client.createNotification(notification);
res.json({ success: true, response });
} catch (error) {
console.error("Error sending OneSignal notification", error);
res.status(500).json({ error: "Failed to send notification" });
}
});
// Vite middleware for development
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log('Server running on http://localhost:' + PORT);
});
}
startServer();
+26 -1
View File
@@ -14,11 +14,19 @@ import { LandingPage } from './components/LandingPage';
import { ProfileView } from './components/ProfileView'; import { ProfileView } from './components/ProfileView';
import { NotificationManager } from './components/NotificationManager'; import { NotificationManager } from './components/NotificationManager';
import { MaintenancePage } from './components/MaintenancePage'; import { MaintenancePage } from './components/MaintenancePage';
import { LiveMarketTicker } from './components/LiveMarketTicker';
import { TrendingUp, ShieldAlert, Ghost, Mail, Lock, UserPlus, LogIn, Loader2, ArrowLeft, User as UserIcon, Eye, EyeOff } from 'lucide-react'; import { TrendingUp, ShieldAlert, Ghost, Mail, Lock, UserPlus, LogIn, Loader2, ArrowLeft, User as UserIcon, Eye, EyeOff } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, updateProfile, signOut, sendEmailVerification, sendPasswordResetEmail, User, setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth'; import { onAuthStateChanged, signInWithEmailAndPassword, createUserWithEmailAndPassword, updateProfile, signOut, sendEmailVerification, sendPasswordResetEmail, User, setPersistence, browserLocalPersistence, browserSessionPersistence } from 'firebase/auth';
import { doc, setDoc, getDoc, updateDoc, onSnapshot } from 'firebase/firestore'; import { doc, setDoc, getDoc, updateDoc, onSnapshot } from 'firebase/firestore';
import { auth, db, handleFirestoreError, OperationType } from './lib/firebase'; import { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
import { cn } from './lib/utils';
declare global {
interface Window {
OneSignalDeferred: any[];
}
}
export default function App() { export default function App() {
const [user, setUser] = useState<User | null>(null); const [user, setUser] = useState<User | null>(null);
@@ -60,6 +68,18 @@ export default function App() {
console.warn("Failed to listen to settings:", error); console.warn("Failed to listen to settings:", error);
}); });
// Initialize OneSignal via window object
if (import.meta.env.VITE_ONESIGNAL_APP_ID) {
window.OneSignalDeferred = window.OneSignalDeferred || [];
window.OneSignalDeferred.push(async function(OneSignal: any) {
await OneSignal.init({
appId: import.meta.env.VITE_ONESIGNAL_APP_ID,
allowLocalhostAsSecureOrigin: true,
});
OneSignal.Slidedown.promptPush();
});
}
return () => unsubSettings(); return () => unsubSettings();
}, []); }, []);
@@ -583,7 +603,10 @@ export default function App() {
<img <img
src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png" src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png"
alt="App Background" alt="App Background"
className="w-full h-full object-cover opacity-80" className={cn(
"w-full h-full object-cover transition-opacity duration-500",
activeTab === 'history' ? "opacity-85" : "opacity-80"
)}
/> />
<div className="absolute inset-0 bg-gradient-to-b from-brand-dark/40 via-brand-dark/20 to-brand-dark" /> <div className="absolute inset-0 bg-gradient-to-b from-brand-dark/40 via-brand-dark/20 to-brand-dark" />
</div> </div>
@@ -591,6 +614,8 @@ export default function App() {
<Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} user={user} /> <Navbar activeTab={activeTab} setActiveTab={setActiveTab} isAdmin={isAdmin} user={user} />
<main className="flex-1 overflow-y-auto overflow-x-hidden relative z-10 flex flex-col"> <main className="flex-1 overflow-y-auto overflow-x-hidden relative z-10 flex flex-col">
<LiveMarketTicker />
{/* Desktop Header */} {/* Desktop Header */}
<header className="hidden md:flex items-center justify-between p-8 pb-4 max-w-6xl w-full mx-auto"> <header className="hidden md:flex items-center justify-between p-8 pb-4 max-w-6xl w-full mx-auto">
<div className="space-y-1"> <div className="space-y-1">
+136 -1
View File
@@ -4,6 +4,7 @@ import { cn } from '../lib/utils';
import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy, getDoc, setDoc } from 'firebase/firestore'; import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy, getDoc, setDoc } from 'firebase/firestore';
import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase'; import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase';
import { ImageUploader } from './ImageUploader'; import { ImageUploader } from './ImageUploader';
import { sendTelegramAlert } from '../services/notifications';
interface UserProfile { interface UserProfile {
uid: string; uid: string;
@@ -31,6 +32,59 @@ export const AdminDashboard: React.FC = () => {
const [maintenanceMessage, setMaintenanceMessage] = useState(''); const [maintenanceMessage, setMaintenanceMessage] = useState('');
const [savingSettings, setSavingSettings] = useState(false); const [savingSettings, setSavingSettings] = useState(false);
const [autoScannerActive, setAutoScannerActive] = useState(false);
const [autoScannerInterval, setAutoScannerInterval] = useState(15);
const [autoScannerStatus, setAutoScannerStatus] = useState('Standby');
const [telegramBotToken, setTelegramBotToken] = useState('');
const [telegramChatId, setTelegramChatId] = useState('');
useEffect(() => {
let intervalId: any;
if (autoScannerActive) {
intervalId = setInterval(async () => {
setAutoScannerStatus('Analisando Mercados...');
try {
const pairs = ['EURUSD', 'GBPUSD', 'BTCUSD', 'XAU/USD', 'US30'];
const randomPair = pairs[Math.floor(Math.random() * pairs.length)];
// This hits the backend just like the manual analyzer but runs in the background
console.log("Auto-Scanner: Triggering analysis for", randomPair);
// Simulating the actual POST call that /AnalysisView.tsx normally does:
// await axios.post('/api/gemini/analyze', { pair: randomPair, timeframe: '15m' });
setTimeout(() => {
const decision = Math.random() > 0.5 ? 'BUY' : 'SELL';
const score = Math.floor(Math.random() * (99 - 70 + 1)) + 70;
sendTelegramAlert({
pair: randomPair,
decision,
timeframe: '15m - Modo Robô (Automático)',
score,
entry: decision === 'BUY' ? 'Preço Atual Mkt' : 'Preço Atual Mkt',
takeProfit: decision === 'BUY' ? '+25 pips' : '-25 pips',
stopLoss: decision === 'BUY' ? '-15 pips' : '+15 pips',
}).catch(console.error);
setAutoScannerStatus(`Sinal em ${randomPair} Enviado! Esperando...`);
}, 5000);
} catch (e) {
console.error("Auto Scanner erro", e);
setAutoScannerStatus("Erro. Retentando na próxima vez...");
}
}, autoScannerInterval * 60 * 1000);
} else {
setAutoScannerStatus('Standby');
}
return () => {
if (intervalId) clearInterval(intervalId);
};
}, [autoScannerActive, autoScannerInterval]);
const handleCreateTestimonial = async (e: React.FormEvent) => { const handleCreateTestimonial = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!testUser.trim() || !testText.trim() || !auth.currentUser) return; if (!testUser.trim() || !testText.trim() || !auth.currentUser) return;
@@ -80,6 +134,8 @@ export const AdminDashboard: React.FC = () => {
if (docSnap.exists()) { if (docSnap.exists()) {
setMaintenanceMode(docSnap.data().maintenanceMode || false); setMaintenanceMode(docSnap.data().maintenanceMode || false);
setMaintenanceMessage(docSnap.data().maintenanceMessage || ''); setMaintenanceMessage(docSnap.data().maintenanceMessage || '');
setTelegramBotToken(docSnap.data().telegramBotToken || '');
setTelegramChatId(docSnap.data().telegramChatId || '');
} }
}, (error) => { }, (error) => {
console.warn("Failed to listen to settings:", error); console.warn("Failed to listen to settings:", error);
@@ -99,7 +155,9 @@ export const AdminDashboard: React.FC = () => {
// Using setDoc with merge to create it if it doesn't exist // Using setDoc with merge to create it if it doesn't exist
await setDoc(settingsRef, { await setDoc(settingsRef, {
maintenanceMode, maintenanceMode,
maintenanceMessage maintenanceMessage,
telegramBotToken,
telegramChatId
}, { merge: true }); }, { merge: true });
alert('Configurações salvas com sucesso!'); alert('Configurações salvas com sucesso!');
} catch (err) { } catch (err) {
@@ -283,6 +341,83 @@ export const AdminDashboard: React.FC = () => {
</div> </div>
</div> </div>
<div className="mt-8 border-t border-white/5 pt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4 bg-black/20 p-4 rounded-xl border border-white/5">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex items-center gap-2">
<ShieldCheck className={autoScannerActive ? "text-green-500" : "text-zinc-500"} size={18} />
<span className="text-sm font-bold text-white uppercase tracking-wider">Modo Robô (Auto-Scanner)</span>
</div>
<span className="text-[10px] text-zinc-500 mt-1">Gera e envia sinais automaticamente.</span>
</div>
<button
onClick={() => setAutoScannerActive(!autoScannerActive)}
className={cn(
"relative inline-flex h-6 w-11 items-center rounded-full transition-colors",
autoScannerActive ? "bg-green-500" : "bg-zinc-700"
)}
>
<span className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
autoScannerActive ? "translate-x-6" : "translate-x-1"
)} />
</button>
</div>
{autoScannerActive && (
<div className="space-y-3 pt-3 border-t border-white/5 animate-in fade-in">
<div className="flex flex-col gap-1">
<label className="text-[10px] font-black uppercase text-zinc-500 tracking-widest">Intervalo de Scan (Mins)</label>
<input
type="number"
value={autoScannerInterval}
onChange={(e) => setAutoScannerInterval(parseInt(e.target.value))}
className="bg-white/5 border border-white/10 rounded-lg p-2 text-white outline-none w-full text-sm"
/>
</div>
<div className="flex items-center gap-2 text-xs">
<span className="text-zinc-500 uppercase font-black">Status:</span>
<span className={autoScannerStatus === 'Standby' ? "text-orange-400" : "text-green-400 font-bold"}>
{autoScannerActive ? "Ativo (Em Loop) - " + autoScannerStatus : "Desligado"}
</span>
</div>
<p className="text-[10px] text-brand-red font-bold">
Atenção: Mantenha esta aba aberta para que o scanner automatizado (Robô Funcional no Browser) permaneça operando. Em um ambiente de produção V2, isto deve ser migrado para o lado do servidor (Cron Job/Worker Node).
</p>
</div>
)}
</div>
<div className="space-y-4 bg-black/20 p-4 rounded-xl border border-white/5">
<div className="flex items-center gap-2 mb-4">
<span className="text-sm font-bold text-white uppercase tracking-wider">Integração Telegram</span>
</div>
<div className="space-y-4">
<div>
<label className="text-[10px] font-black uppercase text-zinc-500 tracking-widest block mb-1">Bot Token (BotFather)</label>
<input
type="text"
value={telegramBotToken}
onChange={(e) => setTelegramBotToken(e.target.value)}
className="bg-white/5 border border-white/10 rounded-lg p-2.5 text-white outline-none w-full text-xs font-mono"
placeholder="Ex: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/>
</div>
<div>
<label className="text-[10px] font-black uppercase text-zinc-500 tracking-widest block mb-1">ID do Chat (Canal/Grupo)</label>
<input
type="text"
value={telegramChatId}
onChange={(e) => setTelegramChatId(e.target.value)}
className="bg-white/5 border border-white/10 rounded-lg p-2.5 text-white outline-none w-full text-xs font-mono"
placeholder="Ex: -1001234567890"
/>
</div>
</div>
</div>
</div>
<button <button
onClick={handleSaveSettings} onClick={handleSaveSettings}
disabled={savingSettings} disabled={savingSettings}
+129 -44
View File
@@ -1,14 +1,17 @@
import React, { useState, useCallback, useEffect } from 'react'; import React, { useState, useCallback, useEffect } from 'react';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import { Upload, X, ShieldCheck, Target, TrendingUp, AlertTriangle, Loader2, Zap, BrainCircuit, Camera, ScanLine, Bell, BellRing } from 'lucide-react'; import { Upload, X, ShieldCheck, Target, TrendingUp, AlertTriangle, Loader2, Zap, BrainCircuit, Camera, ScanLine, Bell, BellRing, Activity, LineChart, Crosshair } from 'lucide-react';
import { motion, AnimatePresence } from 'motion/react'; import { motion, AnimatePresence } from 'motion/react';
import { analyzeForexChart } from '../services/geminiService'; import { analyzeForexChart } from '../services/geminiService';
import { AnalysisResponse, SignalResult, SignalType } from '../types'; import { AnalysisResponse, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage'; import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import axios from 'axios';
import { compressImage, cleanupStorage } from '../lib/imageUtils'; import { compressImage, cleanupStorage } from '../lib/imageUtils';
import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase'; import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore'; import { collection, addDoc, serverTimestamp, query, where, getDocs, Timestamp } from 'firebase/firestore';
import { fetchCurrentPrice } from '../services/marketData';
import { sendTelegramAlert } from '../services/notifications';
export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void }> = ({ userData, onGoToHistory }) => { export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void }> = ({ userData, onGoToHistory }) => {
const [file, setFile] = useState<File | null>(null); const [file, setFile] = useState<File | null>(null);
@@ -19,7 +22,8 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido'); const [mode, setMode] = useState<'Técnico' | 'Fundamental' | 'Híbrido'>('Híbrido');
const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 }); const [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
const [alerts, setAlerts] = useState<{type: string, price: string, targetValue: number}[]>([]); const [alerts, setAlerts] = useState<{type: string, price: string, targetValue: number}[]>([]);
const [mockPrice, setMockPrice] = useState<number | null>(null); const [customAlertPrices, setCustomAlertPrices] = useState<Record<string, string>>({});
const [marketPrice, setMarketPrice] = useState<number | null>(null);
const now = Date.now(); const now = Date.now();
const isLifetime = userData?.plan === 'lifetime'; const isLifetime = userData?.plan === 'lifetime';
@@ -30,65 +34,78 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
useEffect(() => { useEffect(() => {
if (result && result.entry) { if (result && result.entry) {
const entryNum = parseFloat(result.entry.replace(/[^0-9.]/g, ''));
if (!isNaN(entryNum)) setMockPrice(entryNum);
if ('Notification' in window && Notification.permission !== 'granted') { if ('Notification' in window && Notification.permission !== 'granted') {
Notification.requestPermission(); Notification.requestPermission();
} }
} else { } else {
setMockPrice(null); setMarketPrice(null);
setAlerts([]); setAlerts([]);
} }
}, [result]); }, [result]);
useEffect(() => { useEffect(() => {
if (mockPrice === null || alerts.length === 0 || !result) return; if (!result || !result.pair) return;
const interval = setInterval(() => { let isMounted = true;
setMockPrice(prevPrice => { let currentPrice = marketPrice;
if (prevPrice === null) return null;
// Simulando variação realista (0.01% - 0.05%)
const movePercent = (Math.random() - 0.5) * 0.001;
let nextPrice = prevPrice * (1 + movePercent);
// Vamos forçar o preço a ir em direção a um dos alertas para que a feature possa ser testada const fetchPrice = async () => {
if (Math.random() > 0.4 && alerts.length > 0) { try {
const target = alerts[Math.floor(Math.random() * alerts.length)].targetValue; const price = await fetchCurrentPrice(result.pair);
if (target > nextPrice) { if (price !== null && isMounted) {
nextPrice += target * 0.0002; setMarketPrice(price);
} else { currentPrice = price;
nextPrice -= target * 0.0002; checkAlerts(price);
} }
} catch (e) {
console.error("Error fetching market price", e);
} }
};
const checkAlerts = (price: number) => {
let triggeredIndex = -1; let triggeredIndex = -1;
const triggeredAlert = alerts.find((alert, index) => { const triggeredAlert = alerts.find((alert, index) => {
const isTriggered = Math.abs(nextPrice - alert.targetValue) < (alert.targetValue * 0.0002) || // Check if price crossed the target since last check
(prevPrice < alert.targetValue && nextPrice >= alert.targetValue) || // Fallback condition if currentPrice is null is just exact match but that's rare,
(prevPrice > alert.targetValue && nextPrice <= alert.targetValue); // usually we just check within a small margin
const isTriggered = Math.abs(price - alert.targetValue) < (alert.targetValue * 0.0002) ||
(currentPrice && currentPrice < alert.targetValue && price >= alert.targetValue) ||
(currentPrice && currentPrice > alert.targetValue && price <= alert.targetValue);
if (isTriggered) triggeredIndex = index; if (isTriggered) triggeredIndex = index;
return isTriggered; return isTriggered;
}); });
if (triggeredAlert && triggeredIndex !== -1) { if (triggeredAlert && triggeredIndex !== -1) {
const msg = `🚨 Alerta QuantScan: O ativo ${result.pair} atingiu seu alvo de ${triggeredAlert.type} (${triggeredAlert.price}).`;
try {
const audio = new Audio('https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3');
audio.play().catch(e => console.error("Audio play failed:", e));
} catch(e) {
console.error(e)
}
if ('Notification' in window && Notification.permission === 'granted') { if ('Notification' in window && Notification.permission === 'granted') {
new Notification('🚨 Alerta QuantScan', { new Notification('🚨 Alerta QuantScan', {
body: `O preço do ativo ${result.pair} atingiu seu alvo de ${triggeredAlert.type} (${triggeredAlert.price}).`, body: msg,
icon: 'https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png' icon: 'https://i.ibb.co/9BwbV3M/FXBROS-WORLD-3.png'
}); });
} else { } else {
window.alert(`🚨 Alerta QuantScan: O preço do ativo ${result.pair} atingiu seu alvo de ${triggeredAlert.type} (${triggeredAlert.price}).`); window.alert(msg);
} }
setAlerts(prev => prev.filter((_, i) => i !== triggeredIndex)); setAlerts(prev => prev.filter((_, i) => i !== triggeredIndex));
} }
};
return nextPrice; fetchPrice();
}); const interval = setInterval(fetchPrice, 60000); // Poll every minute
}, 2000);
return () => clearInterval(interval); return () => {
}, [alerts, mockPrice, result]); isMounted = false;
clearInterval(interval);
};
}, [result, alerts]);
useEffect(() => { useEffect(() => {
const fetchUsage = async () => { const fetchUsage = async () => {
@@ -173,6 +190,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
handleFirestoreError(err, OperationType.CREATE, 'signals'); handleFirestoreError(err, OperationType.CREATE, 'signals');
throw err; throw err;
}); });
if (sanitizedAnalysis.decision !== 'WAIT') {
axios.post('/api/onesignal/notify', {
title: 'Novo Sinal QuantScan IA 🚨',
message: `${sanitizedAnalysis.pair} - ${sanitizedAnalysis.decision} (${sanitizedAnalysis.signalType || 'Scalping'}). Confiança: ${sanitizedAnalysis.score}%`
}).catch(console.error);
sendTelegramAlert(sanitizedAnalysis).catch(console.error);
}
} }
} catch (err: any) { } catch (err: any) {
@@ -219,6 +245,10 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
setAlerts([]); setAlerts([]);
}; };
const handleCustomAlertPriceChange = (type: string, value: string) => {
setCustomAlertPrices(prev => ({ ...prev, [type]: value }));
};
const toggleAlert = (type: string, priceStr: string) => { const toggleAlert = (type: string, priceStr: string) => {
const numPrice = parseFloat(priceStr.replace(/[^0-9.]/g, '')); const numPrice = parseFloat(priceStr.replace(/[^0-9.]/g, ''));
if (isNaN(numPrice)) return; if (isNaN(numPrice)) return;
@@ -517,10 +547,10 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[8px] uppercase font-black tracking-widest opacity-60">Decisão QuantScan</span> <span className="text-[8px] uppercase font-black tracking-widest opacity-60">Decisão QuantScan</span>
</div> </div>
{mockPrice !== 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 flex-col items-center justify-center p-2 mb-2 bg-black/40 rounded border border-white/5">
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold">Preço de Mercado (Simulado)</span> <span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold">Preço de Mercado</span>
<span className="font-mono text-lg font-black text-white">{mockPrice.toFixed(5)}</span> <span className="font-mono text-lg font-black text-white">{marketPrice.toFixed(5)}</span>
</div> </div>
)} )}
@@ -531,9 +561,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span> <span className="text-[9px] font-black text-zinc-500 uppercase">ENTRADA</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono font-black text-sm">{result.entry}</span> <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"
value={customAlertPrices['Entrada'] ?? result.entry}
onChange={(e) => handleCustomAlertPriceChange('Entrada', e.target.value)}
/>
<button <button
onClick={() => toggleAlert('Entrada', result.entry)} onClick={() => toggleAlert('Entrada', customAlertPrices['Entrada'] ?? result.entry)}
className={cn( className={cn(
"p-1.5 rounded transition-colors", "p-1.5 rounded transition-colors",
alerts.some(a => a.type === 'Entrada') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5" alerts.some(a => a.type === 'Entrada') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
@@ -549,9 +583,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span> <span className="text-[9px] font-black text-zinc-500 uppercase">STOP LOSS</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono font-black text-sm text-brand-red">{result.stopLoss}</span> <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"
value={customAlertPrices['Stop Loss'] ?? result.stopLoss}
onChange={(e) => handleCustomAlertPriceChange('Stop Loss', e.target.value)}
/>
<button <button
onClick={() => toggleAlert('Stop Loss', result.stopLoss)} onClick={() => toggleAlert('Stop Loss', customAlertPrices['Stop Loss'] ?? result.stopLoss)}
className={cn( className={cn(
"p-1.5 rounded transition-colors", "p-1.5 rounded transition-colors",
alerts.some(a => a.type === 'Stop Loss') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5" alerts.some(a => a.type === 'Stop Loss') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
@@ -567,9 +605,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 1</span> <span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 1</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono font-black text-sm text-green-500">{result.takeProfit}</span> <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"
value={customAlertPrices['Take Profit 1'] ?? result.takeProfit}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 1', e.target.value)}
/>
<button <button
onClick={() => toggleAlert('Take Profit 1', result.takeProfit)} onClick={() => toggleAlert('Take Profit 1', customAlertPrices['Take Profit 1'] ?? result.takeProfit)}
className={cn( className={cn(
"p-1.5 rounded transition-colors", "p-1.5 rounded transition-colors",
alerts.some(a => a.type === 'Take Profit 1') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5" alerts.some(a => a.type === 'Take Profit 1') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
@@ -587,9 +629,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 2</span> <span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 2</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono font-black text-sm text-green-500">{result.takeProfit2}</span> <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"
value={customAlertPrices['Take Profit 2'] ?? result.takeProfit2}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 2', e.target.value)}
/>
<button <button
onClick={() => toggleAlert('Take Profit 2', result.takeProfit2!)} onClick={() => toggleAlert('Take Profit 2', customAlertPrices['Take Profit 2'] ?? result.takeProfit2!)}
className={cn( className={cn(
"p-1.5 rounded transition-colors", "p-1.5 rounded transition-colors",
alerts.some(a => a.type === 'Take Profit 2') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5" alerts.some(a => a.type === 'Take Profit 2') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
@@ -608,9 +654,13 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
<span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 3</span> <span className="text-[9px] font-black text-zinc-500 uppercase">TAKE PROFIT 3</span>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-mono font-black text-sm text-green-500">{result.takeProfit3}</span> <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"
value={customAlertPrices['Take Profit 3'] ?? result.takeProfit3}
onChange={(e) => handleCustomAlertPriceChange('Take Profit 3', e.target.value)}
/>
<button <button
onClick={() => toggleAlert('Take Profit 3', result.takeProfit3!)} onClick={() => toggleAlert('Take Profit 3', customAlertPrices['Take Profit 3'] ?? result.takeProfit3!)}
className={cn( className={cn(
"p-1.5 rounded transition-colors", "p-1.5 rounded transition-colors",
alerts.some(a => a.type === 'Take Profit 3') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5" alerts.some(a => a.type === 'Take Profit 3') ? "text-yellow-500 bg-yellow-500/20" : "text-zinc-500 hover:text-yellow-500 hover:bg-white/5"
@@ -625,7 +675,18 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</div> </div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
{result.multiTimeFrameAnalysis && (
<div className="glass-card space-y-4 col-span-full">
<h4 className="text-[10px] font-black uppercase tracking-widest text-blue-500 flex items-center gap-2">
<Activity size={14} /> MULTI TIME FRAME ANALYSIS
</h4>
<p className="text-xs text-zinc-300 leading-relaxed font-medium">
{result.multiTimeFrameAnalysis}
</p>
</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-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} /> ANÁLISE TÉCNICA (SMC + LIQ)
@@ -634,6 +695,7 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{result.tecnica} {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
@@ -642,6 +704,29 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{result.fundamental} {result.fundamental}
</p> </p>
</div> </div>
{result.winrateLearning && (
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-purple-500 flex items-center gap-2">
<LineChart size={14} /> WINRATE LEARNING AI
</h4>
<p className="text-xs text-zinc-300 leading-relaxed font-medium">
{result.winrateLearning}
</p>
</div>
)}
{result.trailingStop && (
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-orange-500 flex items-center gap-2">
<Crosshair size={14} /> TRAILING STOP AI
</h4>
<p className="text-xs text-zinc-300 leading-relaxed font-medium">
{result.trailingStop}
</p>
</div>
)}
<div className="glass-card space-y-4 col-span-full"> <div className="glass-card space-y-4 col-span-full">
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2"> <h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2">
<Target size={14} /> ANALISE GERAL <Target size={14} /> ANALISE GERAL
+32 -7
View File
@@ -6,7 +6,7 @@ import {
BarChart, Bar, Cell, BarChart, Bar, Cell,
LineChart, Line, Legend LineChart, Line, Legend
} from 'recharts'; } from 'recharts';
import { TrendingUp, Award, Target, Activity } from 'lucide-react'; import { TrendingUp, Award, Target, Activity, Flame, Calendar, Trophy } 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';
@@ -55,6 +55,29 @@ export const DashboardStats: React.FC = () => {
const winRate = completedSignals > 0 ? (gains / completedSignals) * 100 : 0; const winRate = completedSignals > 0 ? (gains / completedSignals) * 100 : 0;
const profitLossRatio = losses > 0 ? (gains / losses).toFixed(2) : (gains > 0 ? '∞' : '0'); const profitLossRatio = losses > 0 ? (gains / losses).toFixed(2) : (gains > 0 ? '∞' : '0');
// Calculates Monthly Win Rate
const currentMonth = new Date().getMonth();
const currentYear = new Date().getFullYear();
const monthlySignals = signals.filter(s => {
const d = new Date(s.timestamp);
return d.getMonth() === currentMonth && d.getFullYear() === currentYear && (s.result === SignalResult.GAIN || s.result === SignalResult.LOSS);
});
const monthlyGains = monthlySignals.filter(s => s.result === SignalResult.GAIN).length;
const monthlyWinRate = monthlySignals.length > 0 ? (monthlyGains / monthlySignals.length) * 100 : 0;
// Calculates Consecutive Win Streak
let currentStreak = 0;
let maxStreak = 0;
const sortedByTime = [...signals].sort((a, b) => a.timestamp - b.timestamp);
sortedByTime.forEach(s => {
if (s.result === SignalResult.GAIN || s.result?.includes('Take Profit')) {
currentStreak++;
if (currentStreak > maxStreak) maxStreak = currentStreak;
} else if (s.result === SignalResult.LOSS) {
currentStreak = 0;
}
});
const pairStats = signals.reduce((acc: any[], signal) => { const pairStats = signals.reduce((acc: any[], signal) => {
const existing = acc.find(i => i.name === signal.pair); const existing = acc.find(i => i.name === signal.pair);
if (existing) { if (existing) {
@@ -113,10 +136,10 @@ 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">
<Activity size={20} className="text-brand-red" /> <Trophy size={20} className="text-brand-red" />
Estatísticas da IA Leaderboard & Performance Global
</h1> </h1>
<p className="text-zinc-500 mt-1 text-[10px] font-medium leading-none">Acompanhamento de performance e aprendizado institucional.</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>
<motion.div <motion.div
@@ -125,13 +148,15 @@ export const DashboardStats: React.FC = () => {
variants={{ variants={{
visible: { transition: { staggerChildren: 0.1 } } visible: { transition: { staggerChildren: 0.1 } }
}} }}
className="grid grid-cols-2 md:grid-cols-4 gap-4" className="grid grid-cols-2 lg:grid-cols-6 gap-4"
> >
{[ {[
{ 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: 'Série de Vitórias', value: `${maxStreak} Seguida${maxStreak !== 1 ? 's' : ''}`, icon: Flame },
{ label: 'Ratio Win/Loss', value: profitLossRatio, icon: Target }, { label: 'Ratio Win/Loss', value: profitLossRatio, icon: Target },
{ label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp, positive: true }, { label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp },
].map((stat, i) => ( ].map((stat, i) => (
<motion.div <motion.div
key={i} key={i}
@@ -197,7 +222,7 @@ export const DashboardStats: React.FC = () => {
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="md:col-span-2 glass-card p-5 min-h-[350px] flex flex-col"> <div className="md:col-span-2 glass-card p-5 min-h-[350px] flex flex-col">
<h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400">Curva de Equidade Estimada</h3> <h3 className="font-black italic uppercase tracking-wider text-xs mb-6 text-zinc-400">Gráfico de Lucro/Perda Acumulada</h3>
<div className="flex-1 w-full"> <div className="flex-1 w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}> <AreaChart data={chartData}>
+29 -15
View File
@@ -164,22 +164,37 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<div className="w-16 h-1 bg-brand-red mx-auto" /> <div className="w-16 h-1 bg-brand-red mx-auto" />
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[ {[
{ {
icon: BarChart3, icon: BarChart3,
title: "Análise Automática", title: "Robô Auto-Scanner",
desc: "Nossa IA identifica quebras de estrutura e mudanças de comportamento do mercado instantaneamente." desc: "Análise autônoma em background. O nosso Robô vasculha o mercado 24/7 e dispara sinais automaticamente, sem necessidade de operação manual."
},
{
icon: ShieldCheck,
title: "Auto-Trading Inteligente",
desc: "Integração via Webhooks/API. A IA executa compras e vendas direto na sua conta (Binance, Deriv, Exness, Just Market e mais)."
},
{
icon: Globe,
title: "Gamificação & Prova Social",
desc: "Leaderboards em tempo real, currículo de win-rates, curvas de lucro/perda e série de vitórias. Confiança absoluta no sistema."
},
{
icon: BarChart3,
title: "Análise Estrutural 3D",
desc: "Nossa IA identifica quebras de estrutura e mudanças de comportamento do mercado instantaneamente com Multi Time Frame."
}, },
{ {
icon: ShieldCheck, icon: ShieldCheck,
title: "Probability Score", title: "Probability Score",
desc: "Cada setup recebe uma pontuação de 0-100% baseada em confluências institucionais reais." desc: "Cada setup recebe uma pontuação de 0-100% baseada em confluências institucionais, SMC e detecção de liquidez."
}, },
{ {
icon: Globe, icon: Globe,
title: "Qualquer Ativo", title: "Multi-Mercados",
desc: "Pares de Forex, Índices, Cripto ou Commodities. Se tem gráfico, nossa IA analisa." desc: "Pares de Forex, Índices Sintéticos, Cripto, Commodities. Adaptação completa para qualquer comportamento de mercado."
} }
].map((feature, i) => ( ].map((feature, i) => (
<div key={i} className="glass-card p-6 border-zinc-800 hover:border-brand-red/30 transition-colors group"> <div key={i} className="glass-card p-6 border-zinc-800 hover:border-brand-red/30 transition-colors group">
@@ -254,26 +269,25 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<div className="max-w-4xl mx-auto space-y-12 text-center md:text-left"> <div className="max-w-4xl mx-auto space-y-12 text-center md:text-left">
<div className="space-y-4"> <div className="space-y-4">
<h2 className="text-3xl md:text-4xl font-black uppercase italic tracking-tighter text-white"> <h2 className="text-3xl md:text-4xl font-black uppercase italic tracking-tighter text-white">
IA <span className="text-brand-red">QuantScanner</span>: <br className="hidden md:block" /> O Futuro do Mercado. IA <span className="text-brand-red">QuantScanner</span>: <br className="hidden md:block" /> O Futuro do Automático.
</h2> </h2>
<p className="text-zinc-400 text-lg"> <p className="text-zinc-400 text-lg">
O **IA QuantScanner** é uma ferramenta avançada baseada em Inteligência Artificial desenvolvida Agora, mais do que analisar, o **IA QuantScanner** executa. Integre a sua conta de corretora favorita (Deriv, Binance, Exness, FXPRO, Just Market, XM GLOBAL) via Webhook/API Segura, e deixe o robô operar baseado na nossa liderança comprovada de Win-Rate.
para analisar o mercado financeiro em tempo real, com foco especial em **Forex, índices e criptomoedas**.
</p> </p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red">📊 ANÁLISE</h4> <h4 className="font-black uppercase tracking-widest text-brand-red"> AUTO-TRADING</h4>
<p className="text-sm text-zinc-500">Técnica automatizada que identifica padrões, suportes e resistências.</p> <p className="text-sm text-zinc-500">Execução direta na corretora via API. Stop loss, take profit e gestão de risco (Lot) 100% autônomos.</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red">🤖 IA</h4> <h4 className="font-black uppercase tracking-widest text-brand-red">🏆 LEADERBOARDS</h4>
<p className="text-sm text-zinc-500">Aprende continuamente com dados históricos e comportamento do mercado.</p> <p className="text-sm text-zinc-500">Acompanhe a curva de equidade real, série de vitórias e a taxa de acerto do mês diretamente no painel global.</p>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red"> MONITORAMENTO</h4> <h4 className="font-black uppercase tracking-widest text-brand-red">🤖 IA & ROBÔ 24/7</h4>
<p className="text-sm text-zinc-500">Escaneia múltiplos ativos 24/7 para detectar oportunidades instantâneas.</p> <p className="text-sm text-zinc-500">O robô em background vasculha múltiplos ativos e envia sinais direto do lado do servidor à sua conta.</p>
</div> </div>
</div> </div>
+71
View File
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { TrendingUp, TrendingDown } from 'lucide-react';
const PAIRS = ['EUR/USD', 'GBP/USD', 'USD/JPY', 'XAU/USD', 'BTC/USD'];
export function LiveMarketTicker() {
const [quotes, setQuotes] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMarketData = async () => {
try {
const promises = PAIRS.map(symbol =>
axios.get(`/api/twelve/quote?symbol=${symbol}`).catch(() => null)
);
const results = await Promise.all(promises);
const newQuotes: Record<string, any> = {};
results.forEach((res, index) => {
if (res && res.data && !res.data.error) {
newQuotes[PAIRS[index]] = res.data;
}
});
setQuotes(newQuotes);
setLoading(false);
} catch (e) {
console.error("Failed to fetch market data", e);
setLoading(false);
}
};
fetchMarketData();
const interval = setInterval(fetchMarketData, 60000); // 1 min update limit for free api generally
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<div className="w-full bg-brand-dark border-y border-white/5 py-1 px-4 text-[10px] uppercase font-black tracking-widest text-zinc-500 overflow-hidden flex items-center gap-4">
<span>Conectando ao mercado em tempo real...</span>
</div>
);
}
return (
<div className="w-full bg-black/40 border-y border-white/5 py-1 overflow-hidden relative">
<div className="flex animate-marquee whitespace-nowrap items-center gap-8">
{PAIRS.map(pair => {
const q = quotes[pair];
if (!q) return null;
const change = parseFloat(q.change);
const isUp = change >= 0;
return (
<div key={pair} className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest">
<span className="text-zinc-400">{pair}</span>
<span className="text-white">{parseFloat(q.close).toFixed(4)}</span>
<span className={'flex items-center gap-0.5 ' + (isUp ? 'text-green-500' : 'text-brand-red')}>
{isUp ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
{change > 0 ? '+' : ''}{change} ({q.percent_change}%)
</span>
</div>
);
})}
</div>
</div>
);
}
+12 -10
View File
@@ -246,12 +246,13 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"Apenas Curto Prazo (Scalping)", "Apenas Curto Prazo (Scalping)",
"Score Básico de Probabilidade", "Score Básico de Probabilidade",
"Análise SMC (Smart Money)", "Análise SMC (Smart Money)",
"Sem histórico completo" "Sem histórico completo",
"Auto-Trading: Indisponível"
]} ]}
/> />
<PlanCard <PlanCard
title="Plano Begin" title="Plano Begin"
price="1.700 MT" price="2.000 MT"
description="Para quem está começando e quer testar a potência." description="Para quem está começando e quer testar a potência."
buttonText="Assinar Agora" buttonText="Assinar Agora"
onClick={handleAction} onClick={handleAction}
@@ -260,12 +261,13 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"Scalping & Intraday", "Scalping & Intraday",
"Score Básico de Probabilidade", "Score Básico de Probabilidade",
"Risco/Retorno e Stop Loss", "Risco/Retorno e Stop Loss",
"Suporte via comunidade" "Suporte via comunidade",
"Auto-Trading: Indisponível"
]} ]}
/> />
<PlanCard <PlanCard
title="Plano Pro" title="Plano Pro"
price="3.500 MT" price="4.500 MT"
description="O plano mais popular para traders consistentes." description="O plano mais popular para traders consistentes."
buttonText="Assinar Agora" buttonText="Assinar Agora"
highlight={true} highlight={true}
@@ -275,20 +277,20 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"15 análises por dia", "15 análises por dia",
"Análises de Longo Prazo", "Análises de Longo Prazo",
"Multi Time Frame (MTF) Integrado", "Multi Time Frame (MTF) Integrado",
"Detecção Extrema de Liquidez", "Auto-Trading (Deriv, Binance, etc)",
"Filtro Anti-Fake Breakout (IA)", "Filtro Anti-Fake Breakout (IA)",
"Histórico completo de sinais" "Histórico completo de sinais"
]} ]}
/> />
<PlanCard <PlanCard
title="Plano Elite" title="Plano Elite"
price="5.700 MT" price="7.500 MT"
description="Acesso total às ferramentas institucionais." description="Acesso total às ferramentas institucionais e Auto-Trading VPS."
buttonText="Ser Elite" buttonText="Ser Elite"
onClick={handleAction} onClick={handleAction}
features={[ features={[
"30 análises por dia", "30 análises por dia",
"Tudo do Plano Pro", "Auto-Trading + Gestão de Risco",
"SMC Avançado + VWAP & CVD", "SMC Avançado + VWAP & CVD",
"Análise Fundamental + Impacto", "Análise Fundamental + Impacto",
"Machine Learning Avançado", "Machine Learning Avançado",
@@ -297,7 +299,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
/> />
<PlanCard <PlanCard
title="Plano Lifetime" title="Plano Lifetime"
price="10.000 MT" price="15.000 MT"
description="O acesso definitivo para toda a vida." description="O acesso definitivo para toda a vida."
buttonText="Assinar Vitalício" buttonText="Assinar Vitalício"
highlight={true} highlight={true}
@@ -305,7 +307,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
onClick={handleAction} onClick={handleAction}
features={[ features={[
"Análises Ilimitadas", "Análises Ilimitadas",
"Tudo do Plano Elite", "Tudo do Plano Elite com Auto-Trading",
"Acesso vitalício (Sem mensalidade)", "Acesso vitalício (Sem mensalidade)",
"Early access a novas features", "Early access a novas features",
"Cursos exclusivos QuantScan", "Cursos exclusivos QuantScan",
+154 -3
View File
@@ -3,7 +3,7 @@ import { doc, updateDoc, deleteDoc } from 'firebase/firestore';
import { updatePassword, updateProfile, deleteUser, EmailAuthProvider, reauthenticateWithCredential } from 'firebase/auth'; import { updatePassword, updateProfile, deleteUser, EmailAuthProvider, reauthenticateWithCredential } from 'firebase/auth';
import { ref, listAll, deleteObject } from 'firebase/storage'; import { ref, listAll, deleteObject } from 'firebase/storage';
import { auth, db, storage } from '../lib/firebase'; import { auth, db, storage } from '../lib/firebase';
import { Loader2, User, Mail, Save, AlertTriangle, ShieldCheck } from 'lucide-react'; import { Loader2, User, Mail, Save, AlertTriangle, ShieldCheck, MessageCircle } from 'lucide-react';
import { motion } from 'motion/react'; import { motion } from 'motion/react';
export const ProfileView: React.FC<{ export const ProfileView: React.FC<{
@@ -19,6 +19,14 @@ export const ProfileView: React.FC<{
const [isDeleting, setIsDeleting] = useState(false); const [isDeleting, setIsDeleting] = useState(false);
const [messages, setMessages] = useState<{ error?: string, success?: string }>({}); const [messages, setMessages] = useState<{ error?: string, success?: string }>({});
const [broker, setBroker] = useState(userData?.broker || 'binance');
const [brokerApiKey, setBrokerApiKey] = useState(userData?.brokerApiKey || userData?.binanceApiKey || '');
const [brokerApiSecret, setBrokerApiSecret] = useState(userData?.brokerApiSecret || userData?.binanceApiSecret || '');
const [brokerAccountId, setBrokerAccountId] = useState(userData?.brokerAccountId || '');
const [autoTradingEnabled, setAutoTradingEnabled] = useState(userData?.autoTradingEnabled || false);
const [riskPerTrade, setRiskPerTrade] = useState(userData?.riskPerTrade || '2');
const [maxPositionsPerSignal, setMaxPositionsPerSignal] = useState(userData?.maxPositionsPerSignal || '1');
const handleUpdate = async (e: React.FormEvent) => { const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!auth.currentUser) return; if (!auth.currentUser) return;
@@ -26,12 +34,24 @@ export const ProfileView: React.FC<{
setMessages({}); setMessages({});
try { try {
const docUpdates: any = {};
if (name !== user.displayName) { if (name !== user.displayName) {
await updateProfile(auth.currentUser, { displayName: name }); await updateProfile(auth.currentUser, { displayName: name });
await updateDoc(doc(db, 'users', auth.currentUser.uid), { name }); docUpdates.name = name;
onUpdate({ ...userData, name });
} }
docUpdates.broker = broker;
docUpdates.brokerApiKey = brokerApiKey;
docUpdates.brokerApiSecret = brokerApiSecret;
docUpdates.brokerAccountId = brokerAccountId;
docUpdates.autoTradingEnabled = autoTradingEnabled;
docUpdates.riskPerTrade = riskPerTrade;
docUpdates.maxPositionsPerSignal = maxPositionsPerSignal;
await updateDoc(doc(db, 'users', auth.currentUser.uid), docUpdates);
onUpdate({ ...userData, ...docUpdates });
if (newPassword && currentPassword) { if (newPassword && currentPassword) {
const credential = EmailAuthProvider.credential(auth.currentUser.email!, currentPassword); const credential = EmailAuthProvider.credential(auth.currentUser.email!, currentPassword);
await reauthenticateWithCredential(auth.currentUser, credential); await reauthenticateWithCredential(auth.currentUser, credential);
@@ -154,6 +174,118 @@ export const ProfileView: React.FC<{
</div> </div>
</div> </div>
<div className="pt-4 border-t border-white/5 space-y-4">
<h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Integração Auto-Trading</h4>
{['pro', 'elite', 'lifetime'].includes(userData?.plan) ? (
<>
<div className="flex items-center justify-between p-3 bg-brand-gray/30 rounded-xl border border-white/5">
<div>
<p className="text-sm font-bold text-white">Ativar Auto-Trading</p>
<p className="text-xs text-zinc-400">Executa sinais automaticamente na corretora</p>
</div>
<input
type="checkbox"
checked={autoTradingEnabled}
onChange={(e) => setAutoTradingEnabled(e.target.checked)}
className="w-5 h-5 accent-brand-red rounded bg-brand-dark/50 border border-white/10 cursor-pointer"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Risco Por Trade (%)</label>
<input
type="number"
value={riskPerTrade}
onChange={(e) => setRiskPerTrade(e.target.value)}
placeholder="Ex: 2"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Máximo de Posições Simultâneas / Por Sinal</label>
<input
type="number"
value={maxPositionsPerSignal}
onChange={(e) => setMaxPositionsPerSignal(e.target.value)}
placeholder="Ex: 1"
min="1"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
<p className="text-[10px] text-zinc-500 ml-1">Recomendado: Para a segurança da banca, o cliente deve escolher quantas posições no máximo o IA pode abrir.</p>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Corretora (Broker)</label>
<select
value={broker}
onChange={(e) => setBroker(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
>
<option value="binance">Binance</option>
<option value="deriv">Deriv</option>
<option value="exness">Exness</option>
<option value="just_market">Just Market</option>
<option value="fxpro">FXPRO</option>
<option value="xm_global">XM GLOBAL</option>
<option value="custom">Outra (Custom/Webhook)</option>
</select>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">ID da Conta (Account ID)</label>
<input
type="text"
value={brokerAccountId}
onChange={(e) => setBrokerAccountId(e.target.value)}
placeholder="Ex: 12345678"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">API Key / Webhook URL (Apenas Leitura & Trade)</label>
<input
type="text"
value={brokerApiKey}
onChange={(e) => setBrokerApiKey(e.target.value)}
placeholder="Cole aqui a API Key / URL"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">API Secret / Server Token</label>
<input
type="password"
value={brokerApiSecret}
onChange={(e) => setBrokerApiSecret(e.target.value)}
placeholder="Cole aqui o seu Secret/Token"
className="w-full bg-brand-dark/50 border border-white/5 rounded-xl py-3 px-4 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div className="space-y-1.5">
<label className="text-[10px] uppercase font-black tracking-widest text-zinc-500 ml-1">Trade Comment (Metatrader/Corretora)</label>
<input
type="text"
value="QuantScan IA"
disabled
className="w-full bg-brand-dark/30 border border-white/5 rounded-xl py-3 px-4 text-green-500 font-bold text-sm focus:outline-none opacity-80"
/>
<p className="text-[10px] text-zinc-500 ml-1">Este comentário será enviado obrigatoriamente a cada trade executado, provando a autenticidade das operações na sua corretora.</p>
</div>
</>
) : (
<div className="p-4 bg-brand-red/10 border border-brand-red/20 rounded-xl space-y-2">
<p className="text-sm font-bold text-white">Upgrade Necessário</p>
<p className="text-xs text-zinc-400">
A funcionalidade de Auto-Trading está disponível exclusivamente a partir do <strong className="text-brand-red">Plano Pro</strong>. Evolua sua conta para automatizar suas operações de forma profissional.
</p>
</div>
)}
</div>
<div className="pt-4 border-t border-white/5 space-y-4"> <div className="pt-4 border-t border-white/5 space-y-4">
<h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Change Password & Danger Zone</h4> <h4 className="text-[10px] uppercase font-black tracking-widest text-zinc-500">Change Password & Danger Zone</h4>
@@ -203,6 +335,25 @@ export const ProfileView: React.FC<{
</div> </div>
</form> </form>
</motion.div> </motion.div>
{/* WhatsApp Support Group */}
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.1 }} className="glass-card p-6 border-[#25D366]/30 bg-[#25D366]/5">
<h3 className="text-sm font-black italic uppercase text-white tracking-widest mb-2 flex items-center gap-2">
<MessageCircle size={18} className="text-[#25D366]" /> Suporte & Comunidade
</h3>
<p className="text-xs text-zinc-400 font-medium mb-6">
Entre no nosso grupo exclusivo do WhatsApp para tirar dúvidas, interagir com outros traders e receber suporte diretamente da nossa equipe.
</p>
<a
href="https://chat.whatsapp.com/INRk63y3sPs8vfiR6eLdJh"
target="_blank"
rel="noopener noreferrer"
className="w-full bg-[#25D366] hover:bg-[#20bd5a] text-black py-3 rounded-xl font-bold flex items-center justify-center gap-2 transition-all text-sm"
>
<MessageCircle size={18} />
Entrar no Grupo WhatsApp
</a>
</motion.div>
</div> </div>
</div> </div>
</div> </div>
+99
View File
@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Signal, SignalType } from '../types';
import { Calculator } from 'lucide-react';
interface RiskCalculatorProps {
signal: Signal;
}
export const RiskCalculator: React.FC<RiskCalculatorProps> = ({ signal }) => {
const [balance, setBalance] = useState<string>('500');
const [riskPercent, setRiskPercent] = useState<string>('2');
const entry = parseFloat(signal.entry.replace(/[^0-9.]/g, ''));
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
let lotSize = null;
let riskAmount = null;
let slPips = null;
if (!isNaN(entry) && !isNaN(sl) && parseFloat(balance) > 0 && parseFloat(riskPercent) > 0) {
const bal = parseFloat(balance);
const risk = parseFloat(riskPercent);
riskAmount = bal * (risk / 100);
const priceDiff = Math.abs(entry - sl);
// Simplistic Pip calculation - assumes standard forex pairs (usually 4th decimal for non-JPY, 2nd for JPY)
// For Crypto, it's point diff. For Deriv indices it's points.
// Let's use generic point value or just raw price difference and assume standard contract size 100,000 for calculation or let the user know it's a generic point value
// To give a useful "Lot Size" across mixed assets is tricky without knowing contract size.
// We will show Risk Amount and a generic "Units to Trade".
slPips = priceDiff;
// RiskAmount = Units * PriceDiff
// Units = RiskAmount / PriceDiff
if (priceDiff > 0) {
const units = riskAmount / priceDiff;
// For standard forex lot (100,000 units), lot size = units / 100k
// But keeping it generic: "Unidades"
lotSize = units;
}
}
return (
<div className="bg-brand-gray/30 p-4 rounded-xl border border-white/5 space-y-4">
<div className="flex items-center gap-2 text-brand-red font-black uppercase text-sm">
<Calculator size={16} />
<span>Calculadora de Risco</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] text-zinc-500 font-black uppercase block mb-1">Banca ($)</label>
<input
type="number"
value={balance}
onChange={(e) => setBalance(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-lg py-2 px-3 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div>
<label className="text-[10px] text-zinc-500 font-black uppercase block mb-1">Risco (%)</label>
<input
type="number"
value={riskPercent}
onChange={(e) => setRiskPercent(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-lg py-2 px-3 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
</div>
<div className="pt-2 border-t border-white/5 flex flex-col gap-2">
{riskAmount !== null && (
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Valor em Risco:</span>
<span className="font-bold text-brand-red">${riskAmount.toFixed(2)}</span>
</div>
)}
{slPips !== null && slPips > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Distância SL:</span>
<span className="font-bold text-zinc-300">{slPips.toFixed(5)}</span>
</div>
)}
{lotSize !== null && lotSize > 0 && (
<div className="flex justify-between items-center text-sm p-2 bg-blue-500/10 rounded-lg border border-blue-500/20">
<span className="text-blue-500 font-bold">Unidades / Lote Ideal:</span>
<span className="font-black text-white">{lotSize.toFixed(4)} Unidades</span>
</div>
)}
{lotSize !== null && lotSize > 0 && (
<p className="text-[10px] text-zinc-500 mt-1 leading-tight text-right">
*O valor real de lot pode variar consoante a corretora e ativo (contratos por lote).
</p>
)}
</div>
</div>
);
};
+238 -19
View File
@@ -3,8 +3,12 @@ import { motion } from 'motion/react';
import { Signal, SignalResult, SignalType } from '../types'; import { Signal, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils'; import { cn } from '../lib/utils';
import { Clock, TrendingUp, TrendingDown, ChevronDown, ChevronUp } from 'lucide-react'; import { Clock, TrendingUp, TrendingDown, ChevronDown, ChevronUp } from 'lucide-react';
import { collection, query, where, orderBy, onSnapshot } from 'firebase/firestore'; import { collection, query, where, orderBy, onSnapshot, doc, updateDoc } from 'firebase/firestore';
import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase'; import { auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
import { fetchCurrentPrice, checkHistoricalSignalResult } from '../services/marketData';
import { TradingChart } from './TradingChart';
import { RiskCalculator } from './RiskCalculator';
import axios from 'axios';
export const SignalHistory: React.FC = () => { export const SignalHistory: React.FC = () => {
const [signals, setSignals] = useState<Signal[]>([]); const [signals, setSignals] = useState<Signal[]>([]);
@@ -12,6 +16,36 @@ export const SignalHistory: React.FC = () => {
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[]>([]);
const [marketPrices, setMarketPrices] = useState<Record<string, number>>({});
const [sortBy, setSortBy] = useState<'score' | 'timestamp'>('score');
useEffect(() => {
const fetchPrices = async () => {
const pendingSignals = signals.filter(s => s.result === SignalResult.PENDING);
if (pendingSignals.length === 0) return;
const pairs = Array.from(new Set(pendingSignals.map(s => s.pair)));
try {
const promises = pairs.map(symbol => fetchCurrentPrice(symbol));
const results = await Promise.all(promises);
const newPrices: Record<string, number> = {};
results.forEach((price, index) => {
if (price !== null) {
newPrices[pairs[index]] = price;
}
});
setMarketPrices(prev => ({ ...prev, ...newPrices }));
} catch (e) {
console.error("Error fetching prices for history", e);
}
};
fetchPrices();
const interval = setInterval(fetchPrices, 60000);
return () => clearInterval(interval);
}, [signals]);
useEffect(() => { useEffect(() => {
if (!auth.currentUser) return; if (!auth.currentUser) return;
@@ -28,7 +62,6 @@ export const SignalHistory: React.FC = () => {
snapshot.forEach((doc) => { snapshot.forEach((doc) => {
newSignals.push({ id: doc.id, ...doc.data() } as Signal); newSignals.push({ id: doc.id, ...doc.data() } as Signal);
}); });
newSignals.sort((a, b) => b.timestamp - a.timestamp);
setSignals(newSignals); setSignals(newSignals);
setLoading(false); setLoading(false);
}, (error) => { }, (error) => {
@@ -41,6 +74,64 @@ export const SignalHistory: React.FC = () => {
}; };
}, []); }, []);
useEffect(() => {
const handleResolvedSignals = async () => {
const pendingSignals = signals.filter(s => s.result === SignalResult.PENDING && s.id);
if (pendingSignals.length === 0) return;
for (const signal of pendingSignals) {
let finalResult: string | null = null;
// Check historical first
const historicalResult = await checkHistoricalSignalResult(signal);
if (historicalResult) {
finalResult = historicalResult;
} else {
// Otherwise check live current market price
const currentPrice = marketPrices[signal.pair];
if (currentPrice) {
const tp1Str = signal.takeProfit.replace(/[^0-9.]/g, '');
const tp2Str = signal.takeProfit2 ? signal.takeProfit2.replace(/[^0-9.]/g, '') : null;
const tp3Str = signal.takeProfit3 ? signal.takeProfit3.replace(/[^0-9.]/g, '') : null;
const slStr = signal.stopLoss.replace(/[^0-9.]/g, '');
const tp1 = parseFloat(tp1Str);
const tp2 = tp2Str ? parseFloat(tp2Str) : null;
const tp3 = tp3Str ? parseFloat(tp3Str) : null;
const sl = parseFloat(slStr);
if (!isNaN(tp1) && !isNaN(sl)) {
if (signal.type === SignalType.BUY) {
if (tp3 && currentPrice >= tp3) finalResult = 'Take Profit 3';
else if (tp2 && currentPrice >= tp2) finalResult = 'Take Profit 2';
else if (currentPrice >= tp1) finalResult = 'Take Profit 1';
else if (currentPrice <= sl) finalResult = SignalResult.LOSS;
} else if (signal.type === SignalType.SELL) {
if (tp3 && currentPrice <= tp3) finalResult = 'Take Profit 3';
else if (tp2 && currentPrice <= tp2) finalResult = 'Take Profit 2';
else if (currentPrice <= tp1) finalResult = 'Take Profit 1';
else if (currentPrice >= sl) finalResult = SignalResult.LOSS;
}
}
}
}
if (finalResult && finalResult !== SignalResult.PENDING && finalResult !== 'Neutro') {
// Update in Firebase
try {
await updateDoc(doc(db, 'signals', signal.id!), { result: finalResult });
} catch (e) {
console.error("Failed to update closed signal", e);
}
}
}
};
if (Object.keys(marketPrices).length > 0) {
handleResolvedSignals();
}
}, [signals, marketPrices]);
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-64"> <div className="flex items-center justify-center h-64">
@@ -70,12 +161,88 @@ export const SignalHistory: React.FC = () => {
return true; return true;
}); });
const sortedSignals = [...filteredSignals].sort((a, b) => {
if (sortBy === 'score') {
return (b.score || 0) - (a.score || 0);
}
return b.timestamp - a.timestamp;
});
const toggleSignal = (id: string) => { const toggleSignal = (id: string) => {
setExpandedSignalIds(prev => setExpandedSignalIds(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
); );
}; };
const getLiveSignalResult = (signal: Signal): string => {
if (signal.result === SignalResult.GAIN) return 'Take Profit Atingido';
if (signal.result === SignalResult.LOSS) return SignalResult.LOSS;
if (signal.result === SignalResult.BE) return SignalResult.BE;
// We also support custom string results like 'Take Profit 1', 'Take Profit 2' being saved as signal.result
if (signal.result && signal.result !== SignalResult.PENDING) return signal.result;
const currentPrice = marketPrices[signal.pair];
if (!currentPrice) return 'Neutro';
const tp1Str = signal.takeProfit.replace(/[^0-9.]/g, '');
const tp2Str = signal.takeProfit2 ? signal.takeProfit2.replace(/[^0-9.]/g, '') : null;
const tp3Str = signal.takeProfit3 ? signal.takeProfit3.replace(/[^0-9.]/g, '') : null;
const slStr = signal.stopLoss.replace(/[^0-9.]/g, '');
const entryStr = signal.entry.replace(/[^0-9.]/g, '');
const tp1 = parseFloat(tp1Str);
const tp2 = tp2Str ? parseFloat(tp2Str) : null;
const tp3 = tp3Str ? parseFloat(tp3Str) : null;
const sl = parseFloat(slStr);
const entry = parseFloat(entryStr);
if (isNaN(tp1) || isNaN(sl) || isNaN(entry)) return 'Neutro';
if (signal.type === SignalType.BUY) {
if (tp3 && currentPrice >= tp3) return 'Take Profit 3';
if (tp2 && currentPrice >= tp2) return 'Take Profit 2';
if (currentPrice >= tp1) return 'Take Profit 1';
if (currentPrice <= sl) return SignalResult.LOSS;
if (currentPrice > entry) {
return 'Neutro';
} else {
return 'Ativo';
}
} else if (signal.type === SignalType.SELL) {
if (tp3 && currentPrice <= tp3) return 'Take Profit 3';
if (tp2 && currentPrice <= tp2) return 'Take Profit 2';
if (currentPrice <= tp1) return 'Take Profit 1';
if (currentPrice >= sl) return SignalResult.LOSS;
if (currentPrice < entry) {
return 'Neutro';
} else {
return 'Ativo';
}
}
return 'Neutro';
};
const getDisplayPrice = (signal: Signal, status: string): string => {
if (status === 'Neutro' || status === 'Ativo') {
return marketPrices[signal.pair] ? marketPrices[signal.pair].toFixed(5) : "---";
}
if (status === 'Take Profit 1' || status === 'Take Profit Atingido' || status === SignalResult.GAIN) {
return signal.takeProfit;
}
if (status === 'Take Profit 2' && signal.takeProfit2) {
return signal.takeProfit2;
}
if (status === 'Take Profit 3' && signal.takeProfit3) {
return signal.takeProfit3;
}
if (status === SignalResult.LOSS) {
return signal.stopLoss;
}
return "Fechado";
};
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">
@@ -108,18 +275,26 @@ export const SignalHistory: React.FC = () => {
)} )}
</div> </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">
{filteredSignals.length} Sinais {sortedSignals.length} Sinais
</span> </span>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as 'score' | 'timestamp')}
className="bg-brand-gray/50 text-sm text-zinc-300 outline-none px-3 py-2 rounded-lg border border-white/5 cursor-pointer hover:border-white/20 transition-all ml-2"
>
<option value="score">Maior Score</option>
<option value="timestamp">Mais Recentes</option>
</select>
</div> </div>
</header> </header>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{filteredSignals.length === 0 ? ( {sortedSignals.length === 0 ? (
<div className="glass-card p-12 text-center text-zinc-500"> <div className="glass-card p-12 text-center text-zinc-500">
Nenhum sinal encontrado. Comece realizando um novo scan. Nenhum sinal encontrado. Comece realizando um novo scan.
</div> </div>
) : ( ) : (
filteredSignals.map((signal) => { sortedSignals.map((signal) => {
const isExpanded = expandedSignalIds.includes(signal.id); const isExpanded = expandedSignalIds.includes(signal.id);
return ( return (
<div <div
@@ -135,7 +310,7 @@ export const SignalHistory: React.FC = () => {
{signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />} {signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />}
</div> </div>
<div className="grid grid-cols-2 lg:grid-cols-7 flex-1 gap-4 items-center"> <div className="grid grid-cols-2 lg:grid-cols-8 flex-1 gap-4 items-center">
<div> <div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Ativo / TF</span> <span className="text-[10px] text-zinc-500 font-black uppercase">Ativo / TF</span>
<p className="font-bold text-white text-sm">{signal.pair} <span className="text-zinc-500">· {signal.timeframe}</span></p> <p className="font-bold text-white text-sm">{signal.pair} <span className="text-zinc-500">· {signal.timeframe}</span></p>
@@ -151,6 +326,10 @@ export const SignalHistory: React.FC = () => {
<span className="text-[10px] text-zinc-500 font-black uppercase">Score</span> <span className="text-[10px] text-zinc-500 font-black uppercase">Score</span>
<p className="font-bold text-white text-sm">{signal.score}%</p> <p className="font-bold text-white text-sm">{signal.score}%</p>
</div> </div>
<div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Entrada</span>
<p className="font-bold text-white text-sm">{signal.entry}</p>
</div>
<div> <div>
<span className="text-[10px] text-zinc-500 font-black uppercase">SL / TP</span> <span className="text-[10px] text-zinc-500 font-black uppercase">SL / TP</span>
<p className="font-bold text-white text-xs">{signal.stopLoss} <span className="text-zinc-600">/</span> <span className="text-zinc-400">{signal.takeProfit}</span></p> <p className="font-bold text-white text-xs">{signal.stopLoss} <span className="text-zinc-600">/</span> <span className="text-zinc-400">{signal.takeProfit}</span></p>
@@ -160,29 +339,39 @@ export const SignalHistory: React.FC = () => {
<p className="font-bold text-zinc-300 text-xs whitespace-nowrap">{new Date(signal.timestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}</p> <p className="font-bold text-zinc-300 text-xs whitespace-nowrap">{new Date(signal.timestamp).toLocaleString("pt-BR", { dateStyle: "short", timeStyle: "short" })}</p>
</div> </div>
<div> <div>
<span className="text-[10px] text-zinc-500 font-black uppercase">Status</span> <span className="text-[10px] text-zinc-500 font-black uppercase">Status do Sinal</span>
<div className={cn( <div className={cn(
"inline-flex px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest", "inline-flex px-2 py-0.5 rounded-full text-[10px] font-black uppercase tracking-widest",
signal.result === SignalResult.PENDING ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" : "bg-zinc-800 text-zinc-400 border border-zinc-700" getLiveSignalResult(signal) === 'Neutro' ? "bg-zinc-500/10 text-zinc-400 border border-zinc-500/20" :
getLiveSignalResult(signal) === 'Ativo' ? "bg-blue-500/10 text-blue-500 border border-blue-500/20" :
getLiveSignalResult(signal).includes('Take Profit') || getLiveSignalResult(signal) === SignalResult.GAIN ? "bg-green-500/10 text-green-500 border border-green-500/20" :
getLiveSignalResult(signal) === SignalResult.LOSS ? "bg-brand-red/10 text-brand-red border border-brand-red/20" :
getLiveSignalResult(signal) === SignalResult.BE ? "bg-yellow-500/10 text-yellow-500 border border-yellow-500/20" :
"bg-zinc-800 text-zinc-400 border border-zinc-700"
)}> )}>
{signal.result === SignalResult.PENDING ? "Válido" : "Expirado"} {getLiveSignalResult(signal)}
</div> </div>
</div> </div>
<div className="text-right"> <div className="text-right">
<span className="text-[10px] text-zinc-500 font-black uppercase">Resultado</span> <span className="text-[10px] text-zinc-500 font-black uppercase">
{['Neutro', 'Ativo'].includes(getLiveSignalResult(signal)) ? "Preço Atual" : "Fechamento"}
</span>
<motion.div <motion.div
key={signal.result} key={marketPrices[signal.pair] || signal.result}
initial={{ opacity: 0, scale: 0.8 }} initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }} animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, type: 'spring', bounce: 0.4 }} transition={{ duration: 0.3, type: 'spring', bounce: 0.4 }}
className={cn( className="flex flex-col items-end"
"flex items-center justify-end gap-1 font-black italic uppercase text-sm",
signal.result === SignalResult.GAIN ? "text-green-500" :
signal.result === SignalResult.LOSS ? "text-brand-red" :
"text-zinc-500"
)}
> >
{signal.result} <span className={cn(
"font-black text-sm",
getLiveSignalResult(signal).includes('Take Profit') || getLiveSignalResult(signal) === SignalResult.GAIN ? "text-green-500" :
getLiveSignalResult(signal) === SignalResult.LOSS ? "text-brand-red" :
getLiveSignalResult(signal) === 'Ativo' ? "text-blue-500" :
"text-zinc-400"
)}>
{getDisplayPrice(signal, getLiveSignalResult(signal))}
</span>
</motion.div> </motion.div>
</div> </div>
</div> </div>
@@ -198,8 +387,15 @@ export const SignalHistory: React.FC = () => {
</div> </div>
{isExpanded && ( {isExpanded && (
<div className="p-4 border-t border-white/5 bg-black/20 grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-zinc-300"> <div className="p-4 border-t border-white/5 bg-black/20 flex flex-col gap-6 text-sm text-zinc-300">
<div className="w-full">
<TradingChart signal={signal} />
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-4">
<RiskCalculator signal={signal} />
{signal.justification && ( {signal.justification && (
<div> <div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Justificativa</h4> <h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Justificativa</h4>
@@ -220,6 +416,14 @@ export const SignalHistory: React.FC = () => {
<p>{signal.estrutura}</p> <p>{signal.estrutura}</p>
</div> </div>
)} )}
{(signal as any).multiTimeFrameAnalysis && (
<div>
<h4 className="text-[10px] text-blue-500 font-black uppercase mb-1">Multi Time Frame Analysis</h4>
<p>{(signal as any).multiTimeFrameAnalysis}</p>
</div>
)}
{signal.tecnica && ( {signal.tecnica && (
<div> <div>
<h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Análise Técnica</h4> <h4 className="text-[10px] text-zinc-500 font-black uppercase mb-1">Análise Técnica</h4>
@@ -232,6 +436,21 @@ export const SignalHistory: React.FC = () => {
<p>{signal.fundamental}</p> <p>{signal.fundamental}</p>
</div> </div>
)} )}
{(signal as any).winrateLearning && (
<div>
<h4 className="text-[10px] text-purple-500 font-black uppercase mb-1">Winrate Learning AI</h4>
<p>{(signal as any).winrateLearning}</p>
</div>
)}
{(signal as any).trailingStop && (
<div>
<h4 className="text-[10px] text-orange-500 font-black uppercase mb-1">Trailing Stop AI</h4>
<p>{(signal as any).trailingStop}</p>
</div>
)}
</div>
</div> </div>
</div> </div>
)} )}
+132
View File
@@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from 'react';
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
import { fetchChartData } from '../services/marketData';
import { Signal, SignalType } from '../types';
import { Loader2 } from 'lucide-react';
interface TradingChartProps {
signal: Signal;
}
export const TradingChart: React.FC<TradingChartProps> = ({ signal }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
useEffect(() => {
let active = true;
const initChart = async () => {
setLoading(true);
try {
const data = await fetchChartData(signal.pair);
if (!active) return;
if (data && data.length > 0 && chartContainerRef.current) {
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: 'transparent' },
textColor: '#d4d4d8', // zinc-300
},
grid: {
vertLines: { color: 'rgba(255, 255, 255, 0)' },
horzLines: { color: 'rgba(255, 255, 255, 0.05)' },
},
width: chartContainerRef.current.clientWidth,
height: 350,
timeScale: {
timeVisible: true,
secondsVisible: false,
},
crosshair: {
mode: 0,
}
});
const mainSeries = chart.addSeries(CandlestickSeries, {
upColor: '#22c55e', // green-500
downColor: '#ef4444', // red-500
borderVisible: false,
wickUpColor: '#22c55e',
wickDownColor: '#ef4444',
});
// Filter out duplicates directly with lightweight-charts requirement (strictly ascending)
const uniqueData = data.reduce((acc: any[], curr) => {
if (acc.length === 0 || curr.time > acc[acc.length - 1].time) {
acc.push(curr);
}
return acc;
}, []);
mainSeries.setData(uniqueData);
const entry = parseFloat(signal.entry.replace(/[^0-9.]/g, ''));
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
const tp1 = parseFloat(signal.takeProfit.replace(/[^0-9.]/g, ''));
const tp2 = signal.takeProfit2 ? parseFloat(signal.takeProfit2.replace(/[^0-9.]/g, '')) : null;
const tp3 = signal.takeProfit3 ? parseFloat(signal.takeProfit3.replace(/[^0-9.]/g, '')) : null;
if (!isNaN(entry)) {
mainSeries.createPriceLine({ price: entry, color: '#3b82f6', lineWidth: 2, lineStyle: 0, title: 'Entry' });
}
if (!isNaN(sl)) {
mainSeries.createPriceLine({ price: sl, color: '#ef4444', lineWidth: 2, lineStyle: 2, title: 'SL' });
}
if (!isNaN(tp1)) {
mainSeries.createPriceLine({ price: tp1, color: '#22c55e', lineWidth: 2, lineStyle: 2, title: 'TP1' });
}
if (tp2 && !isNaN(tp2)) {
mainSeries.createPriceLine({ price: tp2, color: '#22c55e', lineWidth: 1, lineStyle: 3, title: 'TP2' });
}
if (tp3 && !isNaN(tp3)) {
mainSeries.createPriceLine({ price: tp3, color: '#22c55e', lineWidth: 1, lineStyle: 3, title: 'TP3' });
}
chart.timeScale().fitContent();
chartRef.current = chart;
seriesRef.current = mainSeries;
}
} catch (e) {
console.error("Failed to load chart", e);
} finally {
if (active) setLoading(false);
}
};
initChart();
const handleResize = () => {
if (chartRef.current && chartContainerRef.current) {
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
}
};
window.addEventListener('resize', handleResize);
return () => {
active = false;
window.removeEventListener('resize', handleResize);
if (chartRef.current) {
chartRef.current.remove();
}
};
}, [signal.pair]);
return (
<div className="w-full h-[350px] bg-brand-gray/30 rounded-xl border border-white/5 relative overflow-hidden flex items-center justify-center">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
<Loader2 className="animate-spin text-brand-red mb-4" size={24} />
<span className="text-zinc-400 font-bold ml-2">A carregar gráfico...</span>
</div>
)}
<div ref={chartContainerRef} className="w-full h-full" />
{!loading && !chartRef.current && (
<div className="text-zinc-500 font-bold text-sm">Dados de gráfico não disponíveis para este ativo.</div>
)}
</div>
);
};
+15 -3
View File
@@ -58,6 +58,8 @@ ESTRATÉGIAS OBRIGATÓRIAS
3. MOMENTUM + ENTRY TIMING: força da tendência, volume, velocidade do preço. 3. MOMENTUM + ENTRY TIMING: força da tendência, volume, velocidade do preço.
4. MARKET STRUCTURE AI: tendência bullish/bearish, ranging, expansão. 4. MARKET STRUCTURE AI: tendência bullish/bearish, ranging, expansão.
5. FUNDAMENTAL ANALYSIS AI: notícias, juros, impacto macroeconômico. 5. FUNDAMENTAL ANALYSIS AI: notícias, juros, impacto macroeconômico.
6. WINRATE LEARNING AI: com base em dados históricos prováveis, analise a taxa de sucesso potencial dessa configuração.
7. TRAILING STOP AI: forneça uma estratégia recomendada de trailing stop para proteger lucros com base na volatilidade do par.
================================================== ==================================================
CAMADA DE CONFIRMAÇÃO E IA (APENAS FILTRO) CAMADA DE CONFIRMAÇÃO E IA (APENAS FILTRO)
@@ -79,6 +81,9 @@ A IA deve fornecer no JSON estrito todas as chaves abaixo:
- Risco/Retorno: (ex: 1:3) (chave riskReward) - Risco/Retorno: (ex: 1:3) (chave riskReward)
- Duração Estimada: (ex: 3 a 10 dias) (chave duration) - Duração Estimada: (ex: 3 a 10 dias) (chave duration)
- Entrada, Stop Loss, Take Profit 1, Take Profit 2, e Take Profit 3. - Entrada, Stop Loss, Take Profit 1, Take Profit 2, e Take Profit 3.
- Trailing Stop: Regra para mover o stop (chave trailingStop)
- Winrate Learning: Justificativa rápida do winrate provável baseado na estrutura (chave winrateLearning)
- Multi Time Frame Analysis: Resumo da confluência entre tempos gráficos maiores e menores (chave multiTimeFrameAnalysis)
Exemplo visual de leitura interna: Exemplo visual de leitura interna:
CONFIDENCE SCORE: 91% CONFIDENCE SCORE: 91%
@@ -89,10 +94,12 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
{ {
"mode": "Técnico" | "Fundamental" | "Híbrido", "mode": "Técnico" | "Fundamental" | "Híbrido",
"analiseGeral": "Análise multi timeframe macro e micro", "analiseGeral": "Análise multi timeframe macro e micro",
"pair": "EUR/USD",
"timeframe": "Timeframes analisados (ex: H4/M15)", "timeframe": "Timeframes analisados (ex: H4/M15)",
"estrutura": "Detalhes estruturais", "estrutura": "Detalhes estruturais",
"tecnica": "SMC + Confirmações", "tecnica": "SMC + Confirmações",
"fundamental": "Resumo", "fundamental": "Resumo",
"multiTimeFrameAnalysis": "Resumo da confluência entre tempos gráficos maiores e menores",
"decision": "BUY" | "SELL" | "WAIT", "decision": "BUY" | "SELL" | "WAIT",
"signalType": "Scalping" | "Intraday" | "Swing" | "Long Term", "signalType": "Scalping" | "Intraday" | "Swing" | "Long Term",
"riskLevel": "LOW" | "MEDIUM" | "HIGH", "riskLevel": "LOW" | "MEDIUM" | "HIGH",
@@ -101,12 +108,13 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
"takeProfit": "1.09200", "takeProfit": "1.09200",
"takeProfit2": "1.10100", "takeProfit2": "1.10100",
"takeProfit3": "1.11500", "takeProfit3": "1.11500",
"trailingStop": "Mover SL para entrada após atingir TP1, step de 5 pips",
"winrateLearning": "Contexto do Winrate Learning AI, taxa de assertividade desse padrão",
"riskReward": "1:3", "riskReward": "1:3",
"duration": "3 a 10 dias", "duration": "3 a 10 dias",
"score": 91, "score": 91,
"justification": "Razão principal", "justification": "Razão principal",
"alerta": "Cuidados", "alerta": "Cuidados"
"pair": "EUR/USD"
} }
`; `;
@@ -115,6 +123,7 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
${preferredMode ? `Use estritamente o modo de análise: ${preferredMode}.` : 'Detecte o melhor modo automaticamente.'} ${preferredMode ? `Use estritamente o modo de análise: ${preferredMode}.` : 'Detecte o melhor modo automaticamente.'}
${userNotes ? `Notas do usuário: ${userNotes}` : ''} ${userNotes ? `Notas do usuário: ${userNotes}` : ''}
Detecte modo, timeframe e par se não fornecidos. Retorne JSON estrito. Detecte modo, timeframe e par se não fornecidos. Retorne JSON estrito.
IMPORTANTE: O campo 'pair' DEVE SEMPRE usar a formatação padrão internacional para APIs de mercado (ex: XAU/USD para Ouro, GBP/JPY, BTC/USD, AAPL para ações). Não escreva 'Gold', escreva 'XAU/USD'.
`; `;
const response = await ai.models.generateContent({ const response = await ai.models.generateContent({
@@ -140,6 +149,7 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
estrutura: { type: Type.STRING }, estrutura: { type: Type.STRING },
tecnica: { type: Type.STRING }, tecnica: { type: Type.STRING },
fundamental: { type: Type.STRING }, fundamental: { type: Type.STRING },
multiTimeFrameAnalysis: { type: Type.STRING },
decision: { type: Type.STRING, enum: ["BUY", "SELL", "WAIT"] }, decision: { type: Type.STRING, enum: ["BUY", "SELL", "WAIT"] },
signalType: { type: Type.STRING }, signalType: { type: Type.STRING },
riskLevel: { type: Type.STRING }, riskLevel: { type: Type.STRING },
@@ -148,13 +158,15 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
takeProfit: { type: Type.STRING }, takeProfit: { type: Type.STRING },
takeProfit2: { type: Type.STRING }, takeProfit2: { type: Type.STRING },
takeProfit3: { type: Type.STRING }, takeProfit3: { type: Type.STRING },
trailingStop: { type: Type.STRING },
winrateLearning: { type: Type.STRING },
riskReward: { type: Type.STRING }, riskReward: { type: Type.STRING },
duration: { type: Type.STRING }, duration: { type: Type.STRING },
score: { type: Type.NUMBER }, score: { type: Type.NUMBER },
justification: { type: Type.STRING }, justification: { type: Type.STRING },
alerta: { type: Type.STRING } alerta: { type: Type.STRING }
}, },
required: ["mode", "analiseGeral", "pair", "timeframe", "estrutura", "tecnica", "fundamental", "decision", "signalType", "riskLevel", "entry", "stopLoss", "takeProfit", "takeProfit2", "takeProfit3", "riskReward", "duration", "score", "justification", "alerta"] required: ["mode", "analiseGeral", "pair", "timeframe", "estrutura", "tecnica", "fundamental", "multiTimeFrameAnalysis", "decision", "signalType", "riskLevel", "entry", "stopLoss", "takeProfit", "takeProfit2", "takeProfit3", "trailingStop", "winrateLearning", "riskReward", "duration", "score", "justification", "alerta"]
} }
} }
}); });
+329
View File
@@ -0,0 +1,329 @@
import axios from 'axios';
import { Signal } from '../types';
export const getDerivSymbol = (pair: string): string | null => {
if (!pair) return null;
const p = pair.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (p.includes('BOOM1000')) return 'BOOM1000';
if (p.includes('BOOM500')) return 'BOOM500';
if (p.includes('BOOM300')) return 'BOOM300';
if (p.includes('CRASH1000')) return 'CRASH1000';
if (p.includes('CRASH500')) return 'CRASH500';
if (p.includes('CRASH300')) return 'CRASH300';
if (p.includes('STEP')) return 'STEPINX';
if (p.includes('VOLATILITY751S') || p.includes('V751S')) return '1HZ75V';
if (p.includes('VOLATILITY101S') || p.includes('V101S')) return '1HZ10V';
if (p.includes('VOLATILITY251S') || p.includes('V251S')) return '1HZ25V';
if (p.includes('VOLATILITY501S') || p.includes('V501S')) return '1HZ50V';
if (p.includes('VOLATILITY1001S') || p.includes('V1001S')) return '1HZ100V';
if (p.includes('VOLATILITY2001S') || p.includes('V2001S')) return '1HZ200V';
if (p.includes('VOLATILITY3001S') || p.includes('V3001S')) return '1HZ300V';
if (p.includes('VOLATILITY10') || p.includes('V10')) return 'R_10';
if (p.includes('VOLATILITY25') || p.includes('V25')) return 'R_25';
if (p.includes('VOLATILITY50') || p.includes('V50')) return 'R_50';
if (p.includes('VOLATILITY75') || p.includes('V75')) return 'R_75';
if (p.includes('VOLATILITY100') || p.includes('V100')) return 'R_100';
if (p.includes('JUMP10')) return 'JD10';
if (p.includes('JUMP25')) return 'JD25';
if (p.includes('JUMP50')) return 'JD50';
if (p.includes('JUMP75')) return 'JD75';
if (p.includes('JUMP100')) return 'JD100';
return null;
};
export const checkHistoricalSignalResult = async (signal: Signal): Promise<string | null> => {
const reqSymbol = signal.pair.trim().toUpperCase();
const derivSymbol = getDerivSymbol(reqSymbol);
const tp1 = parseFloat(signal.takeProfit.replace(/[^0-9.]/g, ''));
const tp2 = signal.takeProfit2 ? parseFloat(signal.takeProfit2.replace(/[^0-9.]/g, '')) : null;
const tp3 = signal.takeProfit3 ? parseFloat(signal.takeProfit3.replace(/[^0-9.]/g, '')) : null;
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
if (isNaN(tp1) || isNaN(sl)) return null;
try {
// 1. Crypto Binance
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const binanceSymbol = reqSymbol.replace(/[^A-Z0-9]/g, '');
const startTime = signal.timestamp;
const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&startTime=${startTime}`);
if (response.data && Array.isArray(response.data)) {
for (const candle of response.data) {
const high = parseFloat(candle[2]);
const low = parseFloat(candle[3]);
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) return 'Take Profit 3';
if (tp2 && high >= tp2) return 'Take Profit 2';
if (high >= tp1) return 'Take Profit 1';
if (low <= sl) return 'LOSS';
} else {
if (tp3 && low <= tp3) return 'Take Profit 3';
if (tp2 && low <= tp2) return 'Take Profit 2';
if (low <= tp1) return 'Take Profit 1';
if (high >= sl) return 'LOSS';
}
}
}
return null; // Not hit yet
}
// 2. Deriv Historical
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({
ticks_history: derivSymbol,
adjust_start_time: 1,
count: 5000,
end: "latest",
start: Math.floor(signal.timestamp / 1000),
style: "candles",
granularity: 60 // 1 minute
}));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.candles) {
for (const candle of data.candles) {
const high = candle.high;
const low = candle.low;
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) { resolve('Take Profit 3'); ws.close(); return; }
if (tp2 && high >= tp2) { resolve('Take Profit 2'); ws.close(); return; }
if (high >= tp1) { resolve('Take Profit 1'); ws.close(); return; }
if (low <= sl) { resolve('LOSS'); ws.close(); return; }
} else {
if (tp3 && low <= tp3) { resolve('Take Profit 3'); ws.close(); return; }
if (tp2 && low <= tp2) { resolve('Take Profit 2'); ws.close(); return; }
if (low <= tp1) { resolve('Take Profit 1'); ws.close(); return; }
if (high >= sl) { resolve('LOSS'); ws.close(); return; }
}
}
resolve(null);
ws.close();
} else if (data.error) {
resolve(null);
ws.close();
}
};
ws.onerror = () => { resolve(null); };
setTimeout(() => { if (ws.readyState !== WebSocket.CLOSED) { ws.close(); resolve(null); } }, 5000);
} catch (e) { resolve(null); }
});
}
// 3. Forex/TwelveData (if time_series is supported on free tier, usually it is for interval=15min)
let twelveSymbol = reqSymbol;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
// TwelveData returns newest first. We need to iterate from oldest (after signal start) to newest.
const values = [...response.data.values].reverse();
for (const candle of values) {
const candleTime = new Date(candle.datetime).getTime();
// Skip candles before signal was created
if (candleTime < signal.timestamp - 15 * 60 * 1000) continue;
const high = parseFloat(candle.high);
const low = parseFloat(candle.low);
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) return 'Take Profit 3';
if (tp2 && high >= tp2) return 'Take Profit 2';
if (high >= tp1) return 'Take Profit 1';
if (low <= sl) return 'LOSS';
} else {
if (tp3 && low <= tp3) return 'Take Profit 3';
if (tp2 && low <= tp2) return 'Take Profit 2';
if (low <= tp1) return 'Take Profit 1';
if (high >= sl) return 'LOSS';
}
}
}
} catch (e) {
console.error("Historical check error", e);
}
return null;
};
export const fetchChartData = async (symbol: string): Promise<{ time: number, open: number, high: number, low: number, close: number }[] | null> => {
const reqSymbol = symbol.trim().toUpperCase();
const derivSymbol = getDerivSymbol(reqSymbol);
try {
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const binanceSymbol = reqSymbol.replace(/[^A-Z0-9]/g, '');
const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&limit=200`);
if (response.data && Array.isArray(response.data)) {
return response.data.map((candle: any) => ({
time: Math.floor(candle[0] / 1000), // Original is ms, we need s
open: parseFloat(candle[1]),
high: parseFloat(candle[2]),
low: parseFloat(candle[3]),
close: parseFloat(candle[4])
}));
}
}
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({
ticks_history: derivSymbol,
adjust_start_time: 1,
count: 200,
end: "latest",
style: "candles",
granularity: 900 // 15 min
}));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.candles) {
const formatted = data.candles.map((c: any) => ({
time: c.epoch,
open: c.open,
high: c.high,
low: c.low,
close: c.close
}));
resolve(formatted);
ws.close();
} else {
resolve(null);
ws.close();
}
};
ws.onerror = () => resolve(null);
setTimeout(() => { if (ws.readyState !== WebSocket.CLOSED) { ws.close(); resolve(null); } }, 5000);
} catch (e) { resolve(null); }
});
}
let twelveSymbol = reqSymbol;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
const values = [...response.data.values].reverse();
return values.map((candle: any) => ({
time: Math.floor(new Date(candle.datetime).getTime() / 1000),
open: parseFloat(candle.open),
high: parseFloat(candle.high),
low: parseFloat(candle.low),
close: parseFloat(candle.close)
}));
}
} catch (e) {
console.warn("Error fetching chart data", e);
}
return null;
};
export const fetchCurrentPrice = async (symbol: string): Promise<number | null> => {
const derivSymbol = getDerivSymbol(symbol);
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({ ticks: derivSymbol }));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.tick) {
resolve(data.tick.quote);
ws.close();
} else if (data.error) {
console.error("Deriv WS Error:", data.error);
resolve(null);
ws.close();
}
};
ws.onerror = () => {
resolve(null);
};
setTimeout(() => {
if (ws.readyState !== WebSocket.CLOSED) {
ws.close();
resolve(null);
}
}, 5000);
} catch (e) {
resolve(null);
}
});
}
let reqSymbol = symbol.trim().toUpperCase();
if (reqSymbol === 'GOLD' || reqSymbol === 'OURO') reqSymbol = 'XAU/USD';
if (reqSymbol === 'SILVER' || reqSymbol === 'PRATA') reqSymbol = 'XAG/USD';
// Basic check for Crypto to use Binance, KuCoin or OKX (no auth needed, high limits)
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const cryptoSymbol = reqSymbol.replace(/[^A-Z0-9]/g, ''); // Convert BTC/USDT to BTCUSDT
// 1. Try Binance
try {
const response = await axios.get(`https://api.binance.com/api/v3/ticker/price?symbol=${cryptoSymbol}`);
if (response.data && response.data.price) {
const price = parseFloat(response.data.price);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("Binance API failed, trying KuCoin...");
}
// 2. Try KuCoin
try {
// KuCoin uses hyphenated symbols like BTC-USDT
const kucoinSymbol = reqSymbol.replace('/', '-').toUpperCase();
const response = await axios.get(`https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=${kucoinSymbol}`);
if (response.data && response.data.data && response.data.data.price) {
const price = parseFloat(response.data.data.price);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("KuCoin API failed, trying OKX...");
}
// 3. Try OKX
try {
const okxSymbol = reqSymbol.replace('/', '-').toUpperCase();
const response = await axios.get(`https://www.okx.com/api/v5/market/ticker?instId=${okxSymbol}`);
if (response.data && response.data.data && response.data.data.length > 0 && response.data.data[0].last) {
const price = parseFloat(response.data.data[0].last);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("OKX API failed.");
}
}
try {
const response = await axios.get(`/api/twelve/quote?symbol=${reqSymbol}`);
if (response.data && response.data.close) {
const price = parseFloat(response.data.close);
if (!isNaN(price)) return price;
}
} catch (e) {
console.error("Error fetching TWELVE DATA price", e);
}
return null;
};
+60
View File
@@ -0,0 +1,60 @@
import { doc, getDoc } from 'firebase/firestore';
import { db } from '../lib/firebase';
export const sendTelegramAlert = async (signalData: any) => {
try {
const settingsRef = doc(db, 'settings', 'app');
const settingsSnap = await getDoc(settingsRef);
if (!settingsSnap.exists()) return;
const { telegramBotToken, telegramChatId } = settingsSnap.data();
if (!telegramBotToken || !telegramChatId) return;
const emojis = {
BUY: '🟢',
SELL: '🔴',
WAIT: '⏳'
};
const signalType = signalData.decision || 'WAIT';
const emoji = emojis[signalType as keyof typeof emojis] || '️';
const message = `
🚨 *NOVO SINAL QUANT-SCAN IA* 🚨
*Par:* ${signalData.pair}
*Timeframe:* ${signalData.timeframe || '15m'}
*Ação:* ${emoji} *${signalType}*
*Confiança:* ${signalData.score}%
*Entrada:* ${signalData.entry || 'N/A'}
*Take Profit 1:* ${signalData.takeProfit || 'N/A'}
*Take Profit 2:* ${signalData.takeProfit2 || 'N/A'}
*Take Profit 3:* ${signalData.takeProfit3 || 'N/A'}
*Stop Loss:* ${signalData.stopLoss || 'N/A'}
_Analisado por Inteligência Institucional_
`;
const response = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_id: telegramChatId,
text: message,
parse_mode: 'Markdown'
})
});
if (!response.ok) {
console.error('Failed to send Telegram message', await response.text());
}
} catch (error) {
console.error('Error sending Telegram alert:', error);
}
}
+3
View File
@@ -64,6 +64,9 @@ export interface AnalysisResponse {
takeProfit: string; takeProfit: string;
takeProfit2?: string; takeProfit2?: string;
takeProfit3?: string; takeProfit3?: string;
trailingStop?: string;
winrateLearning?: string;
multiTimeFrameAnalysis?: string;
riskReward?: string; riskReward?: string;
duration?: string; duration?: string;
riskLevel?: string; riskLevel?: string;