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:
@@ -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,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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Generated
+1189
-113
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -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",
|
||||||
|
|||||||
@@ -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
@@ -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">
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user