feat: Integrate OneSignal and add AI strategy features

Adds OneSignal for push notifications and new AI strategy parameters like trailing stop, winrate learning, and multi-time frame analysis. Also updates build scripts and dependencies.
This commit is contained in:
desartstudio95
2026-05-20 03:26:54 -07:00
parent a579152114
commit babce35231
21 changed files with 2795 additions and 237 deletions
+10
View File
@@ -7,3 +7,13 @@ GEMINI_API_KEY="MY_GEMINI_API_KEY"
# AI Studio automatically injects this at runtime with the Cloud Run service URL.
# Used for self-referential links, OAuth callbacks, and API endpoints.
APP_URL="MY_APP_URL"
# Twelve Data API
TWELVE_DATA_API_KEY=
# OneSignal Frontend Config
VITE_ONESIGNAL_APP_ID=
# OneSignal Backend Config
ONESIGNAL_APP_ID=
ONESIGNAL_REST_API_KEY=
+1 -1
View File
@@ -1,5 +1,5 @@
<div align="center">
<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>
# Run and deploy your AI Studio app
+1
View File
@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Google AI Studio App</title>
<script src="https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.page.js" defer></script>
</head>
<body>
<div id="root"></div>
+1189 -113
View File
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -4,8 +4,9 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite --port=3000 --host=0.0.0.0",
"build": "vite build",
"dev": "tsx server.ts",
"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",
"clean": "rm -rf dist",
"lint": "tsc --noEmit"
@@ -16,6 +17,7 @@
"@google/genai": "^1.29.0",
"@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4",
"axios": "^1.16.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"dotenv": "^17.2.3",
@@ -23,11 +25,14 @@
"embla-carousel-react": "^8.6.0",
"express": "^4.21.2",
"firebase": "^12.12.1",
"lightweight-charts": "^5.2.0",
"lucide-react": "^0.546.0",
"motion": "^12.23.24",
"onesignal-node": "^3.4.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-dropzone": "^15.0.0",
"react-onesignal": "^3.5.2",
"recharts": "^3.8.1",
"shadcn": "^4.7.0",
"tailwind-merge": "^3.5.0",
@@ -39,6 +44,7 @@
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"autoprefixer": "^10.4.21",
"esbuild": "^0.28.0",
"eslint": "^10.2.1",
"tailwindcss": "^4.1.14",
"ts-node": "^10.9.2",
+103
View File
@@ -0,0 +1,103 @@
import express from "express";
import path from "path";
import axios from "axios";
import { createServer as createViteServer } from "vite";
import * as OneSignal from 'onesignal-node';
async function startServer() {
const app = express();
const PORT = 3000;
app.use(express.json());
// API Routes
app.get("/api/health", (req, res) => {
res.json({ status: "ok" });
});
// Twelve Data endpoint
app.get("/api/twelve/quote", async (req, res) => {
const symbol = req.query.symbol || "EUR/USD";
const apiKey = process.env.TWELVE_DATA_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "TWELVE_DATA_API_KEY is not configured" });
}
try {
const response = await axios.get(`https://api.twelvedata.com/quote?symbol=${symbol}&apikey=${apiKey}`);
res.json(response.data);
} catch (error) {
console.error("Error fetching Twelve Data", error);
res.status(500).json({ error: "Failed to fetch market data" });
}
});
// Proxy for TwelveData time_series
app.get("/api/twelve/history", async (req, res) => {
const symbol = req.query.symbol as string;
const apiKey = process.env.TWELVE_DATA_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: "TWELVE_DATA_API_KEY is not configured" });
}
try {
const response = await axios.get(`https://api.twelvedata.com/time_series?symbol=${symbol}&interval=15min&outputsize=500&apikey=${apiKey}`);
res.json(response.data);
} catch (error) {
console.error("Error fetching Twelve Data History", error);
res.status(500).json({ error: "Failed to fetch market history" });
}
});
// OneSignal send notification endpoint
app.post("/api/onesignal/notify", async (req, res) => {
const { title, message } = req.body;
const appId = process.env.ONESIGNAL_APP_ID;
const restApiKey = process.env.ONESIGNAL_REST_API_KEY;
if (!appId || !restApiKey) {
return res.status(500).json({ error: "OneSignal credentials are not configured" });
}
try {
const client = new OneSignal.Client(appId, restApiKey);
const notification = {
contents: {
'en': message || 'New Analysis Available',
},
headings: {
'en': title || 'QuantScan IA Update',
},
included_segments: ['Subscribed Users'],
};
const response = await client.createNotification(notification);
res.json({ success: true, response });
} catch (error) {
console.error("Error sending OneSignal notification", error);
res.status(500).json({ error: "Failed to send notification" });
}
});
// Vite middleware for development
if (process.env.NODE_ENV !== "production") {
const vite = await createViteServer({
server: { middlewareMode: true },
appType: "spa",
});
app.use(vite.middlewares);
} else {
const distPath = path.join(process.cwd(), 'dist');
app.use(express.static(distPath));
app.get('*', (req, res) => {
res.sendFile(path.join(distPath, 'index.html'));
});
}
app.listen(PORT, "0.0.0.0", () => {
console.log('Server running on http://localhost:' + PORT);
});
}
startServer();
+26 -1
View File
@@ -14,11 +14,19 @@ import { LandingPage } from './components/LandingPage';
import { ProfileView } from './components/ProfileView';
import { NotificationManager } from './components/NotificationManager';
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 { motion, AnimatePresence } from 'motion/react';
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 { auth, db, handleFirestoreError, OperationType } from './lib/firebase';
import { cn } from './lib/utils';
declare global {
interface Window {
OneSignalDeferred: any[];
}
}
export default function App() {
const [user, setUser] = useState<User | null>(null);
@@ -60,6 +68,18 @@ export default function App() {
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();
}, []);
@@ -583,7 +603,10 @@ export default function App() {
<img
src="https://i.ibb.co/YFQNMsjX/7eaead9a-0cd4-48d7-8f10-b32d98256e6f.png"
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>
@@ -591,6 +614,8 @@ export default function App() {
<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">
<LiveMarketTicker />
{/* Desktop Header */}
<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">
+136 -1
View File
@@ -4,6 +4,7 @@ import { cn } from '../lib/utils';
import { collection, onSnapshot, doc, updateDoc, addDoc, deleteDoc, query, orderBy, getDoc, setDoc } from 'firebase/firestore';
import { db, handleFirestoreError, OperationType, auth } from '../lib/firebase';
import { ImageUploader } from './ImageUploader';
import { sendTelegramAlert } from '../services/notifications';
interface UserProfile {
uid: string;
@@ -31,6 +32,59 @@ export const AdminDashboard: React.FC = () => {
const [maintenanceMessage, setMaintenanceMessage] = useState('');
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) => {
e.preventDefault();
if (!testUser.trim() || !testText.trim() || !auth.currentUser) return;
@@ -80,6 +134,8 @@ export const AdminDashboard: React.FC = () => {
if (docSnap.exists()) {
setMaintenanceMode(docSnap.data().maintenanceMode || false);
setMaintenanceMessage(docSnap.data().maintenanceMessage || '');
setTelegramBotToken(docSnap.data().telegramBotToken || '');
setTelegramChatId(docSnap.data().telegramChatId || '');
}
}, (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
await setDoc(settingsRef, {
maintenanceMode,
maintenanceMessage
maintenanceMessage,
telegramBotToken,
telegramChatId
}, { merge: true });
alert('Configurações salvas com sucesso!');
} catch (err) {
@@ -283,6 +341,83 @@ export const AdminDashboard: React.FC = () => {
</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
onClick={handleSaveSettings}
disabled={savingSettings}
+129 -44
View File
@@ -1,14 +1,17 @@
import React, { useState, useCallback, useEffect } from 'react';
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 { analyzeForexChart } from '../services/geminiService';
import { AnalysisResponse, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils';
import { ref, uploadBytes, getDownloadURL } from 'firebase/storage';
import axios from 'axios';
import { compressImage, cleanupStorage } from '../lib/imageUtils';
import { storage, auth, db, handleFirestoreError, OperationType } from '../lib/firebase';
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 }) => {
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 [usageInfo, setUsageInfo] = useState<{ used: number, limit: number }>({ used: 0, limit: userData?.analysisLimit ?? 8 });
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 isLifetime = userData?.plan === 'lifetime';
@@ -30,65 +34,78 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
useEffect(() => {
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') {
Notification.requestPermission();
}
} else {
setMockPrice(null);
setMarketPrice(null);
setAlerts([]);
}
}, [result]);
useEffect(() => {
if (mockPrice === null || alerts.length === 0 || !result) return;
if (!result || !result.pair) return;
const interval = setInterval(() => {
setMockPrice(prevPrice => {
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);
let isMounted = true;
let currentPrice = marketPrice;
// Vamos forçar o preço a ir em direção a um dos alertas para que a feature possa ser testada
if (Math.random() > 0.4 && alerts.length > 0) {
const target = alerts[Math.floor(Math.random() * alerts.length)].targetValue;
if (target > nextPrice) {
nextPrice += target * 0.0002;
} else {
nextPrice -= target * 0.0002;
const fetchPrice = async () => {
try {
const price = await fetchCurrentPrice(result.pair);
if (price !== null && isMounted) {
setMarketPrice(price);
currentPrice = price;
checkAlerts(price);
}
} catch (e) {
console.error("Error fetching market price", e);
}
};
const checkAlerts = (price: number) => {
let triggeredIndex = -1;
const triggeredAlert = alerts.find((alert, index) => {
const isTriggered = Math.abs(nextPrice - alert.targetValue) < (alert.targetValue * 0.0002) ||
(prevPrice < alert.targetValue && nextPrice >= alert.targetValue) ||
(prevPrice > alert.targetValue && nextPrice <= alert.targetValue);
// Check if price crossed the target since last check
// Fallback condition if currentPrice is null is just exact match but that's rare,
// 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;
return isTriggered;
});
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') {
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'
});
} 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));
}
};
return nextPrice;
});
}, 2000);
fetchPrice();
const interval = setInterval(fetchPrice, 60000); // Poll every minute
return () => clearInterval(interval);
}, [alerts, mockPrice, result]);
return () => {
isMounted = false;
clearInterval(interval);
};
}, [result, alerts]);
useEffect(() => {
const fetchUsage = async () => {
@@ -173,6 +190,15 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
handleFirestoreError(err, OperationType.CREATE, 'signals');
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) {
@@ -219,6 +245,10 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
setAlerts([]);
};
const handleCustomAlertPriceChange = (type: string, value: string) => {
setCustomAlertPrices(prev => ({ ...prev, [type]: value }));
};
const toggleAlert = (type: string, priceStr: string) => {
const numPrice = parseFloat(priceStr.replace(/[^0-9.]/g, ''));
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>
</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">
<span className="text-[9px] uppercase tracking-widest text-zinc-500 font-bold">Preço de Mercado (Simulado)</span>
<span className="font-mono text-lg font-black text-white">{mockPrice.toFixed(5)}</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">{marketPrice.toFixed(5)}</span>
</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>
</div>
<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
onClick={() => toggleAlert('Entrada', result.entry)}
onClick={() => toggleAlert('Entrada', customAlertPrices['Entrada'] ?? result.entry)}
className={cn(
"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"
@@ -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>
</div>
<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
onClick={() => toggleAlert('Stop Loss', result.stopLoss)}
onClick={() => toggleAlert('Stop Loss', customAlertPrices['Stop Loss'] ?? result.stopLoss)}
className={cn(
"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"
@@ -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>
</div>
<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
onClick={() => toggleAlert('Take Profit 1', result.takeProfit)}
onClick={() => toggleAlert('Take Profit 1', customAlertPrices['Take Profit 1'] ?? result.takeProfit)}
className={cn(
"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"
@@ -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>
</div>
<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
onClick={() => toggleAlert('Take Profit 2', result.takeProfit2!)}
onClick={() => toggleAlert('Take Profit 2', customAlertPrices['Take Profit 2'] ?? result.takeProfit2!)}
className={cn(
"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"
@@ -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>
</div>
<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
onClick={() => toggleAlert('Take Profit 3', result.takeProfit3!)}
onClick={() => toggleAlert('Take Profit 3', customAlertPrices['Take Profit 3'] ?? result.takeProfit3!)}
className={cn(
"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"
@@ -625,7 +675,18 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
</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">
<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)
@@ -634,6 +695,7 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{result.tecnica}
</p>
</div>
<div className="glass-card space-y-4">
<h4 className="text-[10px] font-black uppercase tracking-widest text-green-500 flex items-center gap-2">
<TrendingUp size={14} /> ANÁLISE FUNDAMENTAL
@@ -642,6 +704,29 @@ export const AnalysisView: React.FC<{ userData?: any, onGoToHistory?: () => void
{result.fundamental}
</p>
</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">
<h4 className="text-[10px] font-black uppercase tracking-widest text-zinc-400 flex items-center gap-2">
<Target size={14} /> ANALISE GERAL
+32 -7
View File
@@ -6,7 +6,7 @@ import {
BarChart, Bar, Cell,
LineChart, Line, Legend
} 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 { 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 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 existing = acc.find(i => i.name === signal.pair);
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">
<header>
<h1 className="text-xl font-black italic tracking-tighter text-white flex items-center gap-3 uppercase">
<Activity size={20} className="text-brand-red" />
Estatísticas da IA
<Trophy size={20} className="text-brand-red" />
Leaderboard & Performance Global
</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>
<motion.div
@@ -125,13 +148,15 @@ export const DashboardStats: React.FC = () => {
variants={{
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: '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: 'Melhor Ativo', value: bestAsset, icon: TrendingUp, positive: true },
{ label: 'Melhor Ativo', value: bestAsset, icon: TrendingUp },
].map((stat, i) => (
<motion.div
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="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">
<ResponsiveContainer width="100%" height="100%">
<AreaChart data={chartData}>
+29 -15
View File
@@ -164,22 +164,37 @@ export const LandingPage: React.FC<LandingPageProps> = ({ onGetStarted, onViewPl
<div className="w-16 h-1 bg-brand-red mx-auto" />
</div>
<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,
title: "Análise Automática",
desc: "Nossa IA identifica quebras de estrutura e mudanças de comportamento do mercado instantaneamente."
title: "Robô Auto-Scanner",
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,
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,
title: "Qualquer Ativo",
desc: "Pares de Forex, Índices, Cripto ou Commodities. Se tem gráfico, nossa IA analisa."
title: "Multi-Mercados",
desc: "Pares de Forex, Índices Sintéticos, Cripto, Commodities. Adaptação completa para qualquer comportamento de mercado."
}
].map((feature, i) => (
<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="space-y-4">
<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>
<p className="text-zinc-400 text-lg">
O **IA QuantScanner** é uma ferramenta avançada baseada em Inteligência Artificial desenvolvida
para analisar o mercado financeiro em tempo real, com foco especial em **Forex, índices e criptomoedas**.
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.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red">📊 ANÁLISE</h4>
<p className="text-sm text-zinc-500">Técnica automatizada que identifica padrões, suportes e resistências.</p>
<h4 className="font-black uppercase tracking-widest text-brand-red"> AUTO-TRADING</h4>
<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 className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red">🤖 IA</h4>
<p className="text-sm text-zinc-500">Aprende continuamente com dados históricos e comportamento do mercado.</p>
<h4 className="font-black uppercase tracking-widest text-brand-red">🏆 LEADERBOARDS</h4>
<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 className="space-y-2">
<h4 className="font-black uppercase tracking-widest text-brand-red"> MONITORAMENTO</h4>
<p className="text-sm text-zinc-500">Escaneia múltiplos ativos 24/7 para detectar oportunidades instantâneas.</p>
<h4 className="font-black uppercase tracking-widest text-brand-red">🤖 IA & ROBÔ 24/7</h4>
<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>
+71
View File
@@ -0,0 +1,71 @@
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { TrendingUp, TrendingDown } from 'lucide-react';
const PAIRS = ['EUR/USD', 'GBP/USD', 'USD/JPY', 'XAU/USD', 'BTC/USD'];
export function LiveMarketTicker() {
const [quotes, setQuotes] = useState<Record<string, any>>({});
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchMarketData = async () => {
try {
const promises = PAIRS.map(symbol =>
axios.get(`/api/twelve/quote?symbol=${symbol}`).catch(() => null)
);
const results = await Promise.all(promises);
const newQuotes: Record<string, any> = {};
results.forEach((res, index) => {
if (res && res.data && !res.data.error) {
newQuotes[PAIRS[index]] = res.data;
}
});
setQuotes(newQuotes);
setLoading(false);
} catch (e) {
console.error("Failed to fetch market data", e);
setLoading(false);
}
};
fetchMarketData();
const interval = setInterval(fetchMarketData, 60000); // 1 min update limit for free api generally
return () => clearInterval(interval);
}, []);
if (loading) {
return (
<div className="w-full bg-brand-dark border-y border-white/5 py-1 px-4 text-[10px] uppercase font-black tracking-widest text-zinc-500 overflow-hidden flex items-center gap-4">
<span>Conectando ao mercado em tempo real...</span>
</div>
);
}
return (
<div className="w-full bg-black/40 border-y border-white/5 py-1 overflow-hidden relative">
<div className="flex animate-marquee whitespace-nowrap items-center gap-8">
{PAIRS.map(pair => {
const q = quotes[pair];
if (!q) return null;
const change = parseFloat(q.change);
const isUp = change >= 0;
return (
<div key={pair} className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest">
<span className="text-zinc-400">{pair}</span>
<span className="text-white">{parseFloat(q.close).toFixed(4)}</span>
<span className={'flex items-center gap-0.5 ' + (isUp ? 'text-green-500' : 'text-brand-red')}>
{isUp ? <TrendingUp size={10} /> : <TrendingDown size={10} />}
{change > 0 ? '+' : ''}{change} ({q.percent_change}%)
</span>
</div>
);
})}
</div>
</div>
);
}
+12 -10
View File
@@ -246,12 +246,13 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"Apenas Curto Prazo (Scalping)",
"Score Básico de Probabilidade",
"Análise SMC (Smart Money)",
"Sem histórico completo"
"Sem histórico completo",
"Auto-Trading: Indisponível"
]}
/>
<PlanCard
title="Plano Begin"
price="1.700 MT"
price="2.000 MT"
description="Para quem está começando e quer testar a potência."
buttonText="Assinar Agora"
onClick={handleAction}
@@ -260,12 +261,13 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"Scalping & Intraday",
"Score Básico de Probabilidade",
"Risco/Retorno e Stop Loss",
"Suporte via comunidade"
"Suporte via comunidade",
"Auto-Trading: Indisponível"
]}
/>
<PlanCard
title="Plano Pro"
price="3.500 MT"
price="4.500 MT"
description="O plano mais popular para traders consistentes."
buttonText="Assinar Agora"
highlight={true}
@@ -275,20 +277,20 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
"15 análises por dia",
"Análises de Longo Prazo",
"Multi Time Frame (MTF) Integrado",
"Detecção Extrema de Liquidez",
"Auto-Trading (Deriv, Binance, etc)",
"Filtro Anti-Fake Breakout (IA)",
"Histórico completo de sinais"
]}
/>
<PlanCard
title="Plano Elite"
price="5.700 MT"
description="Acesso total às ferramentas institucionais."
price="7.500 MT"
description="Acesso total às ferramentas institucionais e Auto-Trading VPS."
buttonText="Ser Elite"
onClick={handleAction}
features={[
"30 análises por dia",
"Tudo do Plano Pro",
"Auto-Trading + Gestão de Risco",
"SMC Avançado + VWAP & CVD",
"Análise Fundamental + Impacto",
"Machine Learning Avançado",
@@ -297,7 +299,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
/>
<PlanCard
title="Plano Lifetime"
price="10.000 MT"
price="15.000 MT"
description="O acesso definitivo para toda a vida."
buttonText="Assinar Vitalício"
highlight={true}
@@ -305,7 +307,7 @@ export const PlansView: React.FC<PlansViewProps> = ({ isUnauthenticated, onGetSt
onClick={handleAction}
features={[
"Análises Ilimitadas",
"Tudo do Plano Elite",
"Tudo do Plano Elite com Auto-Trading",
"Acesso vitalício (Sem mensalidade)",
"Early access a novas features",
"Cursos exclusivos QuantScan",
+154 -3
View File
@@ -3,7 +3,7 @@ import { doc, updateDoc, deleteDoc } from 'firebase/firestore';
import { updatePassword, updateProfile, deleteUser, EmailAuthProvider, reauthenticateWithCredential } from 'firebase/auth';
import { ref, listAll, deleteObject } from 'firebase/storage';
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';
export const ProfileView: React.FC<{
@@ -19,6 +19,14 @@ export const ProfileView: React.FC<{
const [isDeleting, setIsDeleting] = useState(false);
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) => {
e.preventDefault();
if (!auth.currentUser) return;
@@ -26,12 +34,24 @@ export const ProfileView: React.FC<{
setMessages({});
try {
const docUpdates: any = {};
if (name !== user.displayName) {
await updateProfile(auth.currentUser, { displayName: name });
await updateDoc(doc(db, 'users', auth.currentUser.uid), { name });
onUpdate({ ...userData, name });
docUpdates.name = 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) {
const credential = EmailAuthProvider.credential(auth.currentUser.email!, currentPassword);
await reauthenticateWithCredential(auth.currentUser, credential);
@@ -154,6 +174,118 @@ export const ProfileView: React.FC<{
</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">
<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>
</form>
</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>
+99
View File
@@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Signal, SignalType } from '../types';
import { Calculator } from 'lucide-react';
interface RiskCalculatorProps {
signal: Signal;
}
export const RiskCalculator: React.FC<RiskCalculatorProps> = ({ signal }) => {
const [balance, setBalance] = useState<string>('500');
const [riskPercent, setRiskPercent] = useState<string>('2');
const entry = parseFloat(signal.entry.replace(/[^0-9.]/g, ''));
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
let lotSize = null;
let riskAmount = null;
let slPips = null;
if (!isNaN(entry) && !isNaN(sl) && parseFloat(balance) > 0 && parseFloat(riskPercent) > 0) {
const bal = parseFloat(balance);
const risk = parseFloat(riskPercent);
riskAmount = bal * (risk / 100);
const priceDiff = Math.abs(entry - sl);
// Simplistic Pip calculation - assumes standard forex pairs (usually 4th decimal for non-JPY, 2nd for JPY)
// For Crypto, it's point diff. For Deriv indices it's points.
// Let's use generic point value or just raw price difference and assume standard contract size 100,000 for calculation or let the user know it's a generic point value
// To give a useful "Lot Size" across mixed assets is tricky without knowing contract size.
// We will show Risk Amount and a generic "Units to Trade".
slPips = priceDiff;
// RiskAmount = Units * PriceDiff
// Units = RiskAmount / PriceDiff
if (priceDiff > 0) {
const units = riskAmount / priceDiff;
// For standard forex lot (100,000 units), lot size = units / 100k
// But keeping it generic: "Unidades"
lotSize = units;
}
}
return (
<div className="bg-brand-gray/30 p-4 rounded-xl border border-white/5 space-y-4">
<div className="flex items-center gap-2 text-brand-red font-black uppercase text-sm">
<Calculator size={16} />
<span>Calculadora de Risco</span>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="text-[10px] text-zinc-500 font-black uppercase block mb-1">Banca ($)</label>
<input
type="number"
value={balance}
onChange={(e) => setBalance(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-lg py-2 px-3 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
<div>
<label className="text-[10px] text-zinc-500 font-black uppercase block mb-1">Risco (%)</label>
<input
type="number"
value={riskPercent}
onChange={(e) => setRiskPercent(e.target.value)}
className="w-full bg-brand-dark/50 border border-white/5 rounded-lg py-2 px-3 text-white text-sm focus:outline-none focus:border-brand-red/50 transition-colors"
/>
</div>
</div>
<div className="pt-2 border-t border-white/5 flex flex-col gap-2">
{riskAmount !== null && (
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Valor em Risco:</span>
<span className="font-bold text-brand-red">${riskAmount.toFixed(2)}</span>
</div>
)}
{slPips !== null && slPips > 0 && (
<div className="flex justify-between items-center text-sm">
<span className="text-zinc-400">Distância SL:</span>
<span className="font-bold text-zinc-300">{slPips.toFixed(5)}</span>
</div>
)}
{lotSize !== null && lotSize > 0 && (
<div className="flex justify-between items-center text-sm p-2 bg-blue-500/10 rounded-lg border border-blue-500/20">
<span className="text-blue-500 font-bold">Unidades / Lote Ideal:</span>
<span className="font-black text-white">{lotSize.toFixed(4)} Unidades</span>
</div>
)}
{lotSize !== null && lotSize > 0 && (
<p className="text-[10px] text-zinc-500 mt-1 leading-tight text-right">
*O valor real de lot pode variar consoante a corretora e ativo (contratos por lote).
</p>
)}
</div>
</div>
);
};
+238 -19
View File
@@ -3,8 +3,12 @@ import { motion } from 'motion/react';
import { Signal, SignalResult, SignalType } from '../types';
import { cn } from '../lib/utils';
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 { fetchCurrentPrice, checkHistoricalSignalResult } from '../services/marketData';
import { TradingChart } from './TradingChart';
import { RiskCalculator } from './RiskCalculator';
import axios from 'axios';
export const SignalHistory: React.FC = () => {
const [signals, setSignals] = useState<Signal[]>([]);
@@ -12,6 +16,36 @@ export const SignalHistory: React.FC = () => {
const [startDate, setStartDate] = useState<string>('');
const [endDate, setEndDate] = 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(() => {
if (!auth.currentUser) return;
@@ -28,7 +62,6 @@ export const SignalHistory: React.FC = () => {
snapshot.forEach((doc) => {
newSignals.push({ id: doc.id, ...doc.data() } as Signal);
});
newSignals.sort((a, b) => b.timestamp - a.timestamp);
setSignals(newSignals);
setLoading(false);
}, (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) {
return (
<div className="flex items-center justify-center h-64">
@@ -70,12 +161,88 @@ export const SignalHistory: React.FC = () => {
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) => {
setExpandedSignalIds(prev =>
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 (
<div className="space-y-6">
<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>
<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>
<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>
</header>
<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">
Nenhum sinal encontrado. Comece realizando um novo scan.
</div>
) : (
filteredSignals.map((signal) => {
sortedSignals.map((signal) => {
const isExpanded = expandedSignalIds.includes(signal.id);
return (
<div
@@ -135,7 +310,7 @@ export const SignalHistory: React.FC = () => {
{signal.type === SignalType.BUY ? <TrendingUp size={24} /> : <TrendingDown size={24} />}
</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>
<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>
@@ -151,6 +326,10 @@ export const SignalHistory: React.FC = () => {
<span className="text-[10px] text-zinc-500 font-black uppercase">Score</span>
<p className="font-bold text-white text-sm">{signal.score}%</p>
</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>
<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>
@@ -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>
</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(
"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 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
key={signal.result}
key={marketPrices[signal.pair] || signal.result}
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, type: 'spring', bounce: 0.4 }}
className={cn(
"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"
)}
className="flex flex-col items-end"
>
{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>
</div>
</div>
@@ -198,8 +387,15 @@ export const SignalHistory: React.FC = () => {
</div>
{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">
<RiskCalculator signal={signal} />
{signal.justification && (
<div>
<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>
</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 && (
<div>
<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>
</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>
)}
+132
View File
@@ -0,0 +1,132 @@
import React, { useEffect, useRef, useState } from 'react';
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
import { fetchChartData } from '../services/marketData';
import { Signal, SignalType } from '../types';
import { Loader2 } from 'lucide-react';
interface TradingChartProps {
signal: Signal;
}
export const TradingChart: React.FC<TradingChartProps> = ({ signal }) => {
const chartContainerRef = useRef<HTMLDivElement>(null);
const [loading, setLoading] = useState(true);
const chartRef = useRef<IChartApi | null>(null);
const seriesRef = useRef<ISeriesApi<"Candlestick"> | null>(null);
useEffect(() => {
let active = true;
const initChart = async () => {
setLoading(true);
try {
const data = await fetchChartData(signal.pair);
if (!active) return;
if (data && data.length > 0 && chartContainerRef.current) {
const chart = createChart(chartContainerRef.current, {
layout: {
background: { type: ColorType.Solid, color: 'transparent' },
textColor: '#d4d4d8', // zinc-300
},
grid: {
vertLines: { color: 'rgba(255, 255, 255, 0)' },
horzLines: { color: 'rgba(255, 255, 255, 0.05)' },
},
width: chartContainerRef.current.clientWidth,
height: 350,
timeScale: {
timeVisible: true,
secondsVisible: false,
},
crosshair: {
mode: 0,
}
});
const mainSeries = chart.addSeries(CandlestickSeries, {
upColor: '#22c55e', // green-500
downColor: '#ef4444', // red-500
borderVisible: false,
wickUpColor: '#22c55e',
wickDownColor: '#ef4444',
});
// Filter out duplicates directly with lightweight-charts requirement (strictly ascending)
const uniqueData = data.reduce((acc: any[], curr) => {
if (acc.length === 0 || curr.time > acc[acc.length - 1].time) {
acc.push(curr);
}
return acc;
}, []);
mainSeries.setData(uniqueData);
const entry = parseFloat(signal.entry.replace(/[^0-9.]/g, ''));
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
const tp1 = parseFloat(signal.takeProfit.replace(/[^0-9.]/g, ''));
const tp2 = signal.takeProfit2 ? parseFloat(signal.takeProfit2.replace(/[^0-9.]/g, '')) : null;
const tp3 = signal.takeProfit3 ? parseFloat(signal.takeProfit3.replace(/[^0-9.]/g, '')) : null;
if (!isNaN(entry)) {
mainSeries.createPriceLine({ price: entry, color: '#3b82f6', lineWidth: 2, lineStyle: 0, title: 'Entry' });
}
if (!isNaN(sl)) {
mainSeries.createPriceLine({ price: sl, color: '#ef4444', lineWidth: 2, lineStyle: 2, title: 'SL' });
}
if (!isNaN(tp1)) {
mainSeries.createPriceLine({ price: tp1, color: '#22c55e', lineWidth: 2, lineStyle: 2, title: 'TP1' });
}
if (tp2 && !isNaN(tp2)) {
mainSeries.createPriceLine({ price: tp2, color: '#22c55e', lineWidth: 1, lineStyle: 3, title: 'TP2' });
}
if (tp3 && !isNaN(tp3)) {
mainSeries.createPriceLine({ price: tp3, color: '#22c55e', lineWidth: 1, lineStyle: 3, title: 'TP3' });
}
chart.timeScale().fitContent();
chartRef.current = chart;
seriesRef.current = mainSeries;
}
} catch (e) {
console.error("Failed to load chart", e);
} finally {
if (active) setLoading(false);
}
};
initChart();
const handleResize = () => {
if (chartRef.current && chartContainerRef.current) {
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
}
};
window.addEventListener('resize', handleResize);
return () => {
active = false;
window.removeEventListener('resize', handleResize);
if (chartRef.current) {
chartRef.current.remove();
}
};
}, [signal.pair]);
return (
<div className="w-full h-[350px] bg-brand-gray/30 rounded-xl border border-white/5 relative overflow-hidden flex items-center justify-center">
{loading && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 z-10">
<Loader2 className="animate-spin text-brand-red mb-4" size={24} />
<span className="text-zinc-400 font-bold ml-2">A carregar gráfico...</span>
</div>
)}
<div ref={chartContainerRef} className="w-full h-full" />
{!loading && !chartRef.current && (
<div className="text-zinc-500 font-bold text-sm">Dados de gráfico não disponíveis para este ativo.</div>
)}
</div>
);
};
+15 -3
View File
@@ -58,6 +58,8 @@ ESTRATÉGIAS OBRIGATÓRIAS
3. MOMENTUM + ENTRY TIMING: força da tendência, volume, velocidade do preço.
4. MARKET STRUCTURE AI: tendência bullish/bearish, ranging, expansão.
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)
@@ -79,6 +81,9 @@ A IA deve fornecer no JSON estrito todas as chaves abaixo:
- Risco/Retorno: (ex: 1:3) (chave riskReward)
- Duração Estimada: (ex: 3 a 10 dias) (chave duration)
- 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:
CONFIDENCE SCORE: 91%
@@ -89,10 +94,12 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
{
"mode": "Técnico" | "Fundamental" | "Híbrido",
"analiseGeral": "Análise multi timeframe macro e micro",
"pair": "EUR/USD",
"timeframe": "Timeframes analisados (ex: H4/M15)",
"estrutura": "Detalhes estruturais",
"tecnica": "SMC + Confirmações",
"fundamental": "Resumo",
"multiTimeFrameAnalysis": "Resumo da confluência entre tempos gráficos maiores e menores",
"decision": "BUY" | "SELL" | "WAIT",
"signalType": "Scalping" | "Intraday" | "Swing" | "Long Term",
"riskLevel": "LOW" | "MEDIUM" | "HIGH",
@@ -101,12 +108,13 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
"takeProfit": "1.09200",
"takeProfit2": "1.10100",
"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",
"duration": "3 a 10 dias",
"score": 91,
"justification": "Razão principal",
"alerta": "Cuidados",
"pair": "EUR/USD"
"alerta": "Cuidados"
}
`;
@@ -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.'}
${userNotes ? `Notas do usuário: ${userNotes}` : ''}
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({
@@ -140,6 +149,7 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
estrutura: { type: Type.STRING },
tecnica: { type: Type.STRING },
fundamental: { type: Type.STRING },
multiTimeFrameAnalysis: { type: Type.STRING },
decision: { type: Type.STRING, enum: ["BUY", "SELL", "WAIT"] },
signalType: { type: Type.STRING },
riskLevel: { type: Type.STRING },
@@ -148,13 +158,15 @@ FORMATO DE SAÍDA (Obrigatório em JSON):
takeProfit: { type: Type.STRING },
takeProfit2: { type: Type.STRING },
takeProfit3: { type: Type.STRING },
trailingStop: { type: Type.STRING },
winrateLearning: { type: Type.STRING },
riskReward: { type: Type.STRING },
duration: { type: Type.STRING },
score: { type: Type.NUMBER },
justification: { type: Type.STRING },
alerta: { type: Type.STRING }
},
required: ["mode", "analiseGeral", "pair", "timeframe", "estrutura", "tecnica", "fundamental", "decision", "signalType", "riskLevel", "entry", "stopLoss", "takeProfit", "takeProfit2", "takeProfit3", "riskReward", "duration", "score", "justification", "alerta"]
required: ["mode", "analiseGeral", "pair", "timeframe", "estrutura", "tecnica", "fundamental", "multiTimeFrameAnalysis", "decision", "signalType", "riskLevel", "entry", "stopLoss", "takeProfit", "takeProfit2", "takeProfit3", "trailingStop", "winrateLearning", "riskReward", "duration", "score", "justification", "alerta"]
}
}
});
+329
View File
@@ -0,0 +1,329 @@
import axios from 'axios';
import { Signal } from '../types';
export const getDerivSymbol = (pair: string): string | null => {
if (!pair) return null;
const p = pair.toUpperCase().replace(/[^A-Z0-9]/g, '');
if (p.includes('BOOM1000')) return 'BOOM1000';
if (p.includes('BOOM500')) return 'BOOM500';
if (p.includes('BOOM300')) return 'BOOM300';
if (p.includes('CRASH1000')) return 'CRASH1000';
if (p.includes('CRASH500')) return 'CRASH500';
if (p.includes('CRASH300')) return 'CRASH300';
if (p.includes('STEP')) return 'STEPINX';
if (p.includes('VOLATILITY751S') || p.includes('V751S')) return '1HZ75V';
if (p.includes('VOLATILITY101S') || p.includes('V101S')) return '1HZ10V';
if (p.includes('VOLATILITY251S') || p.includes('V251S')) return '1HZ25V';
if (p.includes('VOLATILITY501S') || p.includes('V501S')) return '1HZ50V';
if (p.includes('VOLATILITY1001S') || p.includes('V1001S')) return '1HZ100V';
if (p.includes('VOLATILITY2001S') || p.includes('V2001S')) return '1HZ200V';
if (p.includes('VOLATILITY3001S') || p.includes('V3001S')) return '1HZ300V';
if (p.includes('VOLATILITY10') || p.includes('V10')) return 'R_10';
if (p.includes('VOLATILITY25') || p.includes('V25')) return 'R_25';
if (p.includes('VOLATILITY50') || p.includes('V50')) return 'R_50';
if (p.includes('VOLATILITY75') || p.includes('V75')) return 'R_75';
if (p.includes('VOLATILITY100') || p.includes('V100')) return 'R_100';
if (p.includes('JUMP10')) return 'JD10';
if (p.includes('JUMP25')) return 'JD25';
if (p.includes('JUMP50')) return 'JD50';
if (p.includes('JUMP75')) return 'JD75';
if (p.includes('JUMP100')) return 'JD100';
return null;
};
export const checkHistoricalSignalResult = async (signal: Signal): Promise<string | null> => {
const reqSymbol = signal.pair.trim().toUpperCase();
const derivSymbol = getDerivSymbol(reqSymbol);
const tp1 = parseFloat(signal.takeProfit.replace(/[^0-9.]/g, ''));
const tp2 = signal.takeProfit2 ? parseFloat(signal.takeProfit2.replace(/[^0-9.]/g, '')) : null;
const tp3 = signal.takeProfit3 ? parseFloat(signal.takeProfit3.replace(/[^0-9.]/g, '')) : null;
const sl = parseFloat(signal.stopLoss.replace(/[^0-9.]/g, ''));
if (isNaN(tp1) || isNaN(sl)) return null;
try {
// 1. Crypto Binance
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const binanceSymbol = reqSymbol.replace(/[^A-Z0-9]/g, '');
const startTime = signal.timestamp;
const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&startTime=${startTime}`);
if (response.data && Array.isArray(response.data)) {
for (const candle of response.data) {
const high = parseFloat(candle[2]);
const low = parseFloat(candle[3]);
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) return 'Take Profit 3';
if (tp2 && high >= tp2) return 'Take Profit 2';
if (high >= tp1) return 'Take Profit 1';
if (low <= sl) return 'LOSS';
} else {
if (tp3 && low <= tp3) return 'Take Profit 3';
if (tp2 && low <= tp2) return 'Take Profit 2';
if (low <= tp1) return 'Take Profit 1';
if (high >= sl) return 'LOSS';
}
}
}
return null; // Not hit yet
}
// 2. Deriv Historical
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({
ticks_history: derivSymbol,
adjust_start_time: 1,
count: 5000,
end: "latest",
start: Math.floor(signal.timestamp / 1000),
style: "candles",
granularity: 60 // 1 minute
}));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.candles) {
for (const candle of data.candles) {
const high = candle.high;
const low = candle.low;
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) { resolve('Take Profit 3'); ws.close(); return; }
if (tp2 && high >= tp2) { resolve('Take Profit 2'); ws.close(); return; }
if (high >= tp1) { resolve('Take Profit 1'); ws.close(); return; }
if (low <= sl) { resolve('LOSS'); ws.close(); return; }
} else {
if (tp3 && low <= tp3) { resolve('Take Profit 3'); ws.close(); return; }
if (tp2 && low <= tp2) { resolve('Take Profit 2'); ws.close(); return; }
if (low <= tp1) { resolve('Take Profit 1'); ws.close(); return; }
if (high >= sl) { resolve('LOSS'); ws.close(); return; }
}
}
resolve(null);
ws.close();
} else if (data.error) {
resolve(null);
ws.close();
}
};
ws.onerror = () => { resolve(null); };
setTimeout(() => { if (ws.readyState !== WebSocket.CLOSED) { ws.close(); resolve(null); } }, 5000);
} catch (e) { resolve(null); }
});
}
// 3. Forex/TwelveData (if time_series is supported on free tier, usually it is for interval=15min)
let twelveSymbol = reqSymbol;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
// TwelveData returns newest first. We need to iterate from oldest (after signal start) to newest.
const values = [...response.data.values].reverse();
for (const candle of values) {
const candleTime = new Date(candle.datetime).getTime();
// Skip candles before signal was created
if (candleTime < signal.timestamp - 15 * 60 * 1000) continue;
const high = parseFloat(candle.high);
const low = parseFloat(candle.low);
if (signal.type === 'BUY') {
if (tp3 && high >= tp3) return 'Take Profit 3';
if (tp2 && high >= tp2) return 'Take Profit 2';
if (high >= tp1) return 'Take Profit 1';
if (low <= sl) return 'LOSS';
} else {
if (tp3 && low <= tp3) return 'Take Profit 3';
if (tp2 && low <= tp2) return 'Take Profit 2';
if (low <= tp1) return 'Take Profit 1';
if (high >= sl) return 'LOSS';
}
}
}
} catch (e) {
console.error("Historical check error", e);
}
return null;
};
export const fetchChartData = async (symbol: string): Promise<{ time: number, open: number, high: number, low: number, close: number }[] | null> => {
const reqSymbol = symbol.trim().toUpperCase();
const derivSymbol = getDerivSymbol(reqSymbol);
try {
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const binanceSymbol = reqSymbol.replace(/[^A-Z0-9]/g, '');
const response = await axios.get(`https://api.binance.com/api/v3/klines?symbol=${binanceSymbol}&interval=15m&limit=200`);
if (response.data && Array.isArray(response.data)) {
return response.data.map((candle: any) => ({
time: Math.floor(candle[0] / 1000), // Original is ms, we need s
open: parseFloat(candle[1]),
high: parseFloat(candle[2]),
low: parseFloat(candle[3]),
close: parseFloat(candle[4])
}));
}
}
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({
ticks_history: derivSymbol,
adjust_start_time: 1,
count: 200,
end: "latest",
style: "candles",
granularity: 900 // 15 min
}));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.candles) {
const formatted = data.candles.map((c: any) => ({
time: c.epoch,
open: c.open,
high: c.high,
low: c.low,
close: c.close
}));
resolve(formatted);
ws.close();
} else {
resolve(null);
ws.close();
}
};
ws.onerror = () => resolve(null);
setTimeout(() => { if (ws.readyState !== WebSocket.CLOSED) { ws.close(); resolve(null); } }, 5000);
} catch (e) { resolve(null); }
});
}
let twelveSymbol = reqSymbol;
if (twelveSymbol === 'GOLD' || twelveSymbol === 'OURO') twelveSymbol = 'XAU/USD';
if (twelveSymbol === 'SILVER' || twelveSymbol === 'PRATA') twelveSymbol = 'XAG/USD';
const response = await axios.get(`/api/twelve/history?symbol=${twelveSymbol}`);
if (response.data && response.data.values && Array.isArray(response.data.values)) {
const values = [...response.data.values].reverse();
return values.map((candle: any) => ({
time: Math.floor(new Date(candle.datetime).getTime() / 1000),
open: parseFloat(candle.open),
high: parseFloat(candle.high),
low: parseFloat(candle.low),
close: parseFloat(candle.close)
}));
}
} catch (e) {
console.warn("Error fetching chart data", e);
}
return null;
};
export const fetchCurrentPrice = async (symbol: string): Promise<number | null> => {
const derivSymbol = getDerivSymbol(symbol);
if (derivSymbol) {
return new Promise((resolve) => {
try {
const ws = new WebSocket('wss://ws.binaryws.com/websockets/v3?app_id=1089');
ws.onopen = () => {
ws.send(JSON.stringify({ ticks: derivSymbol }));
};
ws.onmessage = (msg) => {
const data = JSON.parse(msg.data);
if (data.tick) {
resolve(data.tick.quote);
ws.close();
} else if (data.error) {
console.error("Deriv WS Error:", data.error);
resolve(null);
ws.close();
}
};
ws.onerror = () => {
resolve(null);
};
setTimeout(() => {
if (ws.readyState !== WebSocket.CLOSED) {
ws.close();
resolve(null);
}
}, 5000);
} catch (e) {
resolve(null);
}
});
}
let reqSymbol = symbol.trim().toUpperCase();
if (reqSymbol === 'GOLD' || reqSymbol === 'OURO') reqSymbol = 'XAU/USD';
if (reqSymbol === 'SILVER' || reqSymbol === 'PRATA') reqSymbol = 'XAG/USD';
// Basic check for Crypto to use Binance, KuCoin or OKX (no auth needed, high limits)
const isCrypto = reqSymbol.includes('BTC') || reqSymbol.includes('ETH') || reqSymbol.includes('USDT') || reqSymbol.includes('BNB');
if (isCrypto) {
const cryptoSymbol = reqSymbol.replace(/[^A-Z0-9]/g, ''); // Convert BTC/USDT to BTCUSDT
// 1. Try Binance
try {
const response = await axios.get(`https://api.binance.com/api/v3/ticker/price?symbol=${cryptoSymbol}`);
if (response.data && response.data.price) {
const price = parseFloat(response.data.price);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("Binance API failed, trying KuCoin...");
}
// 2. Try KuCoin
try {
// KuCoin uses hyphenated symbols like BTC-USDT
const kucoinSymbol = reqSymbol.replace('/', '-').toUpperCase();
const response = await axios.get(`https://api.kucoin.com/api/v1/market/orderbook/level1?symbol=${kucoinSymbol}`);
if (response.data && response.data.data && response.data.data.price) {
const price = parseFloat(response.data.data.price);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("KuCoin API failed, trying OKX...");
}
// 3. Try OKX
try {
const okxSymbol = reqSymbol.replace('/', '-').toUpperCase();
const response = await axios.get(`https://www.okx.com/api/v5/market/ticker?instId=${okxSymbol}`);
if (response.data && response.data.data && response.data.data.length > 0 && response.data.data[0].last) {
const price = parseFloat(response.data.data[0].last);
if (!isNaN(price)) return price;
}
} catch (e) {
console.warn("OKX API failed.");
}
}
try {
const response = await axios.get(`/api/twelve/quote?symbol=${reqSymbol}`);
if (response.data && response.data.close) {
const price = parseFloat(response.data.close);
if (!isNaN(price)) return price;
}
} catch (e) {
console.error("Error fetching TWELVE DATA price", e);
}
return null;
};
+60
View File
@@ -0,0 +1,60 @@
import { doc, getDoc } from 'firebase/firestore';
import { db } from '../lib/firebase';
export const sendTelegramAlert = async (signalData: any) => {
try {
const settingsRef = doc(db, 'settings', 'app');
const settingsSnap = await getDoc(settingsRef);
if (!settingsSnap.exists()) return;
const { telegramBotToken, telegramChatId } = settingsSnap.data();
if (!telegramBotToken || !telegramChatId) return;
const emojis = {
BUY: '🟢',
SELL: '🔴',
WAIT: '⏳'
};
const signalType = signalData.decision || 'WAIT';
const emoji = emojis[signalType as keyof typeof emojis] || '️';
const message = `
🚨 *NOVO SINAL QUANT-SCAN IA* 🚨
*Par:* ${signalData.pair}
*Timeframe:* ${signalData.timeframe || '15m'}
*Ação:* ${emoji} *${signalType}*
*Confiança:* ${signalData.score}%
*Entrada:* ${signalData.entry || 'N/A'}
*Take Profit 1:* ${signalData.takeProfit || 'N/A'}
*Take Profit 2:* ${signalData.takeProfit2 || 'N/A'}
*Take Profit 3:* ${signalData.takeProfit3 || 'N/A'}
*Stop Loss:* ${signalData.stopLoss || 'N/A'}
_Analisado por Inteligência Institucional_
`;
const response = await fetch(`https://api.telegram.org/bot${telegramBotToken}/sendMessage`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_id: telegramChatId,
text: message,
parse_mode: 'Markdown'
})
});
if (!response.ok) {
console.error('Failed to send Telegram message', await response.text());
}
} catch (error) {
console.error('Error sending Telegram alert:', error);
}
}
+3
View File
@@ -64,6 +64,9 @@ export interface AnalysisResponse {
takeProfit: string;
takeProfit2?: string;
takeProfit3?: string;
trailingStop?: string;
winrateLearning?: string;
multiTimeFrameAnalysis?: string;
riskReward?: string;
duration?: string;
riskLevel?: string;