feat(deriv): integrate Deriv API websocket connectivity

Add Deriv API websocket client, route handling, and Firestore security rules to support automated trading integration.
This commit is contained in:
desartstudio95
2026-06-10 14:58:02 -07:00
parent f7a97e3532
commit 2d6e1a0809
19 changed files with 1588 additions and 10 deletions
+7 -2
View File
@@ -129,8 +129,13 @@ service cloud.firestore {
}
// Auto Trades Collection
match /users/{userId}/auto_trades/{tradeId} {
allow read, write: if true;
match /users/{userId}/auto_trades/{document=**} {
allow read, write: if isOwner(userId) || isDbAdmin();
}
// Auto Trades Results Collection
match /users/{userId}/auto_trades_results/{document=**} {
allow read, write: if isOwner(userId) || isDbAdmin();
}
// Global Auto Trades Collection
+16 -4
View File
@@ -36,12 +36,14 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"vite": "^6.2.0"
"vite": "^6.2.0",
"ws": "^8.21.0"
},
"devDependencies": {
"@firebase/rules-unit-testing": "^3.0.4",
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/ws": "^8.18.1",
"autoprefixer": "^10.4.21",
"esbuild": "^0.28.0",
"eslint": "^10.2.1",
@@ -3792,6 +3794,16 @@
"integrity": "sha512-lrpDziQipxCEeK5kWxvljWYhUvOiB2A9izZd9B2AFarYAkqZshb4lPbRs7zKEic6eGtH8V/2qJW+dPp9OtF6bw==",
"license": "MIT"
},
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz",
@@ -13094,9 +13106,9 @@
"license": "ISC"
},
"node_modules/ws": {
"version": "8.20.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
"integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
"version": "8.21.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
+3 -1
View File
@@ -40,12 +40,14 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tw-animate-css": "^1.4.0",
"vite": "^6.2.0"
"vite": "^6.2.0",
"ws": "^8.21.0"
},
"devDependencies": {
"@firebase/rules-unit-testing": "^3.0.4",
"@types/express": "^4.17.21",
"@types/node": "^22.14.0",
"@types/ws": "^8.18.1",
"autoprefixer": "^10.4.21",
"esbuild": "^0.28.0",
"eslint": "^10.2.1",
+5
View File
@@ -11,6 +11,8 @@ const MetaApi = typeof MetaApiPkg === "function" ? MetaApiPkg : (MetaApiPkg as a
import { db } from "./src/lib/firebase";
import { doc, getDoc, setDoc, addDoc, collection, runTransaction, query, where, getDocs, updateDoc, orderBy, limit } from "firebase/firestore";
import derivRoutes from "./src/routes/deriv.routes.ts";
async function startServer() {
const app = express();
const PORT = 3000;
@@ -22,6 +24,9 @@ async function startServer() {
res.json({ status: "ok" });
});
// Mount Deriv Routes
app.use("/api/deriv", derivRoutes);
// --- BACKGROUND WORKER NODE (CRON JOB) ---
function getYfSymbol(pair: string): string {
if (pair.includes('XAU')) return 'GC=F';
+397
View File
@@ -0,0 +1,397 @@
import React, { useState, useEffect, useRef } from 'react';
import { AITradingEngine, RiskManager, AISignal } from '../services/aiTradingEngine';
import { Activity, Target, TrendingUp, AlertCircle, Clock, Zap, X } from 'lucide-react';
import axios from 'axios';
import { cn } from '../lib/utils';
import { collection, addDoc } from 'firebase/firestore';
import { db } from '../lib/firebase';
export interface ErrorLogDetail {
timestamp: number;
endpoint?: string;
payload?: any;
httpCode?: number;
message: string;
stackTrace?: string;
}
export const AITradingDashboard = ({ userData, engineRunning }: { userData: any, engineRunning: boolean }) => {
const [lastSignal, setLastSignal] = useState<AISignal | null>(null);
const [lastLog, setLastLog] = useState<{message: string, isError: boolean, timestamp: number} | null>(null);
const [errorLogs, setErrorLogs] = useState<ErrorLogDetail[]>([]);
const [showLogsModal, setShowLogsModal] = useState(false);
const [isDebug, setIsDebug] = useState(false);
const logDetailedError = (customMessage: string, error?: any, endpoint?: string, payload?: any) => {
let parsedPayload = payload;
if (!parsedPayload && error?.config?.data) {
try { parsedPayload = JSON.parse(error.config.data); } catch (e) { parsedPayload = error.config.data; }
}
const log: ErrorLogDetail = {
timestamp: Date.now(),
endpoint: endpoint || error?.config?.url,
payload: parsedPayload,
httpCode: error?.response?.status,
message: error?.response?.data?.details?.message || error?.response?.data?.error || error?.response?.data?.message || error?.message || customMessage,
stackTrace: error?.stack
};
setErrorLogs(prev => [log, ...prev].slice(0, 100));
setLastLog({ message: `Erro: ${log.message}`, isError: true, timestamp: log.timestamp });
console.error(customMessage, error);
};
const [stats, setStats] = useState({
winRate: 68.5,
tradesToday: 0,
profitToday: 0,
lastTradeAction: '-',
lastTradeResult: '-',
lastTradeTime: 0
});
const statsRef = useRef(stats);
useEffect(() => { statsRef.current = stats; }, [stats]);
const token = userData?.savedDerivToken || userData?.mtPassword;
const appId = userData?.derivAppId || '1089';
const symbol = userData?.tradingSettings?.selectedAsset || 'R_100';
const activeCycleRef = useRef<boolean>(false);
const lastSignalRef = useRef<number>(0);
const monitoringTradesRef = useRef<number[]>([]);
useEffect(() => {
if (!engineRunning || userData?.mtPlatform !== 'deriv_api' || !token) return;
const executeCycle = async () => {
if (activeCycleRef.current) return;
activeCycleRef.current = true;
try {
// 1. Monitor Phase
const positionsToKeep: number[] = [];
for (const contractId of monitoringTradesRef.current) {
try {
const statusRes = await axios.get(`/api/deriv/contract/${contractId}?appId=${appId}`, {
headers: { Authorization: `Bearer ${token}` }
});
const contract = statusRes.data?.contract;
if (!contract) continue;
if (contract.is_sold) {
setLastLog({ message: `Trade ${contractId} fechado externamente. Lucro: $${contract.profit.toFixed(2)}`, isError: false, timestamp: Date.now() });
setStats(prev => ({
...prev,
profitToday: prev.profitToday + contract.profit,
lastTradeResult: contract.profit >= 0 ? 'WIN' : 'LOSS'
}));
continue;
}
const buyPrice = contract.buy_price;
const roi = contract.profit / buyPrice;
const ageSeconds = (Date.now() / 1000) - contract.date_start;
// Close if +/- 10% ROI, or after 60 seconds
if (roi >= 0.1 || roi <= -0.1 || ageSeconds > 60) {
try {
await axios.post(`/api/deriv/close-trade?appId=${appId}`, { contractId }, {
headers: { Authorization: `Bearer ${token}` }
});
const finalRes = await axios.get(`/api/deriv/contract/${contractId}?appId=${appId}`, {
headers: { Authorization: `Bearer ${token}` }
});
const finalProfit = finalRes.data?.contract?.profit || contract.profit;
setLastLog({ message: `Trade ${contractId} finalizado. Lucro: $${finalProfit.toFixed(2)}`, isError: false, timestamp: Date.now() });
setStats(prev => ({
...prev,
profitToday: prev.profitToday + finalProfit,
lastTradeResult: finalProfit >= 0 ? 'WIN' : 'LOSS'
}));
if (userData?.uid) {
await addDoc(collection(db, "users", userData.uid, "auto_trades"), {
contract_id: contractId,
profit: finalProfit,
timestamp: Date.now(),
symbol: contract.underlying
}).catch(e => logDetailedError("Erro ao salvar trade no firestore", e, "firebase/addDoc"));
}
} catch (e: any) {
logDetailedError("Failed to close trade", e, `/api/deriv/close-trade?appId=${appId}`, { contractId });
positionsToKeep.push(contractId);
}
} else {
positionsToKeep.push(contractId);
}
} catch (e: any) {
logDetailedError("Failed to fetch contract status", e, `/api/deriv/contract/${contractId}?appId=${appId}`);
positionsToKeep.push(contractId);
}
}
monitoringTradesRef.current = positionsToKeep;
// 2. Generate Signal & Open Phase (Every 30s)
const now = Date.now();
if (now - lastSignalRef.current > 30000) {
lastSignalRef.current = now;
const tickRes = await axios.get(`/api/deriv/tick/${symbol}?appId=${appId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (tickRes.data?.success === false) {
logDetailedError("Could not fetch price", { message: tickRes.data?.error }, `/api/deriv/tick/${symbol}?appId=${appId}`);
throw new Error(tickRes.data?.error || "Could not fetch price");
}
const price = tickRes.data?.tick?.quote || 0;
if (!price) {
logDetailedError("Could not fetch price limit", { message: "Price empty" }, `/api/deriv/tick/${symbol}?appId=${appId}`);
throw new Error("Could not fetch price");
}
const signal = await AITradingEngine.analyze(symbol, price);
setLastSignal(signal);
const openPositions = monitoringTradesRef.current.length;
const balance = 10000; // Mock or fetch balance
let amount = parseFloat(userData?.tradingSettings?.fixedLot || "1");
const MIN_STAKE = 1;
const requestedAmount = amount;
if (amount < MIN_STAKE) {
amount = MIN_STAKE;
}
if (requestedAmount !== amount) {
console.log(`Requested Amount: ${requestedAmount}`);
console.log(`Final Amount Sent: ${amount}`);
setLastLog({ message: `Stake validado: Requested Amount: ${requestedAmount} -> Final Amount Sent: ${amount}`, isError: false, timestamp: Date.now() });
}
const riskLimits = {
maxDailyLoss: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 100,
maxOpenPositions: parseInt(userData?.tradingSettings?.maxPositions) || 3,
maxTradeValue: parseFloat(userData?.tradingSettings?.dailyLossLimit) || 50,
minConfidence: parseFloat(userData?.tradingSettings?.minConfidence) || 80
};
const cooldownMinutes = parseFloat(userData?.tradingSettings?.cooldownMinutes) || 5;
const timeSinceLastTrade = Date.now() - statsRef.current.lastTradeTime;
if (statsRef.current.lastTradeTime > 0 && timeSinceLastTrade < cooldownMinutes * 60 * 1000) {
setLastLog({ message: `Sinal ignorado (Cooldown): Aguarde ${Math.ceil((cooldownMinutes * 60 * 1000 - timeSinceLastTrade) / 1000)}s`, isError: true, timestamp: Date.now() });
return; // skip execution
}
const validation = RiskManager.validate(signal, openPositions, balance, riskLimits, amount, statsRef.current.profitToday, engineRunning);
if (!validation.valid) {
setLastLog({ message: `Sinal rejeitado: ${validation.reason}`, isError: true, timestamp: Date.now() });
} else {
const isBuy = signal.action === 'BUY';
const tradePayload: any = {
symbol: signal.symbol,
amount: amount,
contract_typeBase: isBuy ? 'MULTUP' : 'MULTDOWN'
};
const tradeRes = await axios.post(`/api/deriv/test-buy?appId=${appId}`, tradePayload, {
headers: { Authorization: `Bearer ${token}` }
});
if (tradeRes.data?.success) {
const newContractId = tradeRes.data.trade.contract_id;
monitoringTradesRef.current.push(newContractId);
setLastLog({ message: `Posição ${newContractId} aberta (${signal.action} ${signal.symbol})`, isError: false, timestamp: Date.now() });
setStats(prev => ({
...prev,
tradesToday: prev.tradesToday + 1,
lastTradeAction: signal.action,
lastTradeResult: 'PENDING',
lastTradeTime: Date.now()
}));
} else {
logDetailedError('Erro ao abrir posição', { message: tradeRes.data?.error || 'Unknown error', response: { status: 400, data: tradeRes.data } }, `/api/deriv/test-buy?appId=${appId}`, tradePayload);
}
}
}
} catch (err: any) {
logDetailedError(err?.message || 'Error no ciclo', err);
} finally {
activeCycleRef.current = false;
}
};
executeCycle();
const intervalId = setInterval(executeCycle, 5000); // 5s loop for monitoring
return () => {
clearInterval(intervalId);
activeCycleRef.current = false;
};
}, [engineRunning, token, appId, symbol, userData]);
return (
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-5 space-y-4 shadow-[0_0_20px_rgba(255,0,0,0.05)]">
<h3 className="font-black italic text-white tracking-[0.2em] text-sm flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex items-center gap-2">
<Zap size={16} className="text-brand-red" />
AI ENGINE DASHBOARD
</div>
<div className="flex items-center gap-2">
{engineRunning ? (
<span className="flex h-2 w-2 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-brand-red opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-brand-red"></span>
</span>
) : (
<span className="h-2 w-2 rounded-full bg-zinc-600"></span>
)}
<span className="text-[10px] uppercase font-mono text-zinc-500">
Cycle: 5s / 30s
</span>
</div>
</h3>
{/* Performance Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1 flex items-center gap-1"><Target size={10} className="text-brand-red" /> Win Rate</div>
<div className="text-lg font-mono font-black text-white">{stats.winRate}%</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1 flex items-center gap-1"><Activity size={10} className="text-brand-red" /> Hoje</div>
<div className="text-lg font-mono font-black text-white">{stats.tradesToday}</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1 flex items-center gap-1"><TrendingUp size={10} className={stats.profitToday >= 0 ? "text-green-500" : "text-red-500"} /> Lucro / Prejuízo</div>
<div className={cn("text-lg font-mono font-black", stats.profitToday >= 0 ? "text-green-400" : "text-red-400")}>
{stats.profitToday >= 0 ? '+' : ''}{stats.profitToday.toFixed(2)}
</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1 flex items-center gap-1"><Clock size={10} className="text-brand-red" /> Último Trade</div>
<div className="text-sm font-mono font-bold text-white mt-1">
{stats.lastTradeAction !== '-' ? (
<span className={cn("px-1.5 py-0.5 rounded text-[10px]", stats.lastTradeAction === 'BUY' ? "bg-green-500/20 text-green-400" : "bg-red-500/20 text-red-400")}>
{stats.lastTradeAction}
</span>
) : '-'} <span className="ml-1 text-zinc-500 text-[10px]">{stats.lastTradeResult}</span>
</div>
</div>
</div>
{/* Last Signal */}
{lastSignal && (
<div className="bg-black border border-white/10 p-4 rounded-xl flex flex-col gap-2 relative overflow-hidden">
<div className="absolute top-0 right-0 w-24 h-24 bg-brand-red/5 blur-2xl rounded-full" />
<div className="flex justify-between items-center text-[10px] uppercase tracking-widest text-zinc-500">
<span>Último Sinal Gerado</span>
<span className="font-mono">{new Date(lastSignal.timestamp).toLocaleTimeString()}</span>
</div>
<div className="flex items-center gap-4">
<span className={cn("text-xl font-black font-mono tracking-wider", lastSignal.action === 'BUY' ? "text-green-500" : "text-red-500")}>
{lastSignal.action}
</span>
<span className="text-sm font-bold text-white">{lastSignal.symbol}</span>
<div className="flex-1" />
<div className="flex flex-col items-end">
<span className="text-[9px] text-zinc-500 uppercase tracking-widest">Confiança</span>
<span className={cn("font-mono font-bold text-sm", lastSignal.confidence >= 80 ? "text-green-400" : "text-yellow-400")}>{lastSignal.confidence}%</span>
</div>
</div>
<p className="text-[11px] text-zinc-400 mt-1 border-t border-white/5 pt-2 italic">
{lastSignal.reason}
</p>
</div>
)}
{/* Exec Log */}
<div className="flex items-center justify-between mt-2">
<div className="flex-1">
{lastLog && (
<div className={cn("text-[10px] font-mono px-3 py-2 rounded-lg flex items-center gap-2",
lastLog.isError ? "bg-red-500/10 text-red-400 border border-red-500/20" : "bg-green-500/10 text-green-400 border border-green-500/20")}>
{lastLog.isError ? <AlertCircle size={12} /> : <Zap size={12} />}
<span className="flex-1 truncate" title={lastLog.message}>{lastLog.message}</span>
<span className="opacity-50">{new Date(lastLog.timestamp).toLocaleTimeString()}</span>
</div>
)}
</div>
{errorLogs.length > 0 && (
<button
onClick={() => setShowLogsModal(true)}
className="ml-2 px-3 py-2 text-[10px] uppercase font-bold tracking-wider rounded-lg bg-white/5 hover:bg-white/10 text-white border border-white/10 flex items-center gap-1 transition-colors"
>
<AlertCircle size={12} className="text-red-400" />
Ver Log Completo ({errorLogs.length})
</button>
)}
</div>
{/* Error Logs Modal */}
{showLogsModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm">
<div className="bg-[#0a0a0a] border border-white/10 rounded-2xl w-full max-w-4xl max-h-[85vh] flex flex-col shadow-2xl overflow-hidden">
<div className="flex items-center justify-between p-4 border-b border-white/10 bg-white/5">
<h2 className="text-white font-bold tracking-wider uppercase text-sm flex items-center gap-2">
<AlertCircle size={16} className="text-brand-red" />
Logs de Sistema
</h2>
<div className="flex items-center gap-4">
<label className="flex items-center gap-2 text-xs text-zinc-400 cursor-pointer">
<input type="checkbox" checked={isDebug} onChange={x => setIsDebug(x.target.checked)} className="rounded border-none bg-black/50 accent-brand-red" />
Modo Debug
</label>
<button onClick={() => setShowLogsModal(false)} className="text-zinc-400 hover:text-white">
<X size={20} />
</button>
</div>
</div>
<div className="flex-1 overflow-y-auto p-4 space-y-4 font-mono">
{errorLogs.map((log, i) => (
<div key={i} className="bg-red-500/5 border border-red-500/20 rounded-lg p-3 text-xs">
<div className="flex items-center justify-between text-zinc-500 mb-2 border-b border-white/5 pb-2">
<span className="text-brand-red font-bold">[{new Date(log.timestamp).toLocaleTimeString()}]</span>
{log.httpCode && <span className={cn("px-2 py-0.5 rounded text-black font-bold", log.httpCode >= 500 ? "bg-red-400" : "bg-orange-400")}>HTTP {log.httpCode}</span>}
</div>
<div className="text-red-400 font-bold mb-3">{log.message}</div>
{log.endpoint && (
<div className="mb-2">
<div className="text-zinc-600 uppercase tracking-widest text-[9px]">Endpoint:</div>
<div className="text-zinc-300 break-all bg-black/50 p-1.5 rounded mt-1">{log.endpoint}</div>
</div>
)}
{log.payload && (
<div className="mb-2">
<div className="text-zinc-600 uppercase tracking-widest text-[9px]">Payload:</div>
<pre className="text-zinc-400 bg-black/50 p-2 rounded mt-1 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(log.payload, null, 2)}
</pre>
</div>
)}
{isDebug && log.stackTrace && (
<div className="mt-4 pt-3 border-t border-white/5">
<div className="text-zinc-600 uppercase tracking-widest text-[9px]">Stack Trace:</div>
<pre className="text-zinc-500 bg-black/50 p-2 rounded mt-1 overflow-x-auto text-[10px] whitespace-pre-wrap">
{log.stackTrace}
</pre>
</div>
)}
</div>
))}
{errorLogs.length === 0 && <div className="text-center text-zinc-500 py-10 font-sans">Nenhum erro registrado nesta sessão.</div>}
</div>
</div>
</div>
)}
</div>
);
};
+67 -3
View File
@@ -5,6 +5,9 @@ import {
BarChart2, TrendingUp, DollarSign, Send, Globe2, AlertTriangle,
Fingerprint, ChevronRight, Server, Cpu, Radio, Network, CheckCircle2, ShieldCheck
} from 'lucide-react';
import { DerivDiagnosticPanel } from './DerivDiagnosticPanel';
import { DerivTestPanel } from './DerivTestPanel';
import { AITradingDashboard } from './AITradingDashboard';
import { cn } from '../lib/utils';
export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any) => void, onNavigate?: (tab: string) => void, isAdmin?: boolean }> = ({ userData, onUpdate, onNavigate, isAdmin }) => {
@@ -61,6 +64,10 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
const [breakEvenEnabled, setBreakEvenEnabled] = useState(userData?.tradingSettings?.breakEvenEnabled ?? true);
const [tradingHoursStart, setTradingHoursStart] = useState(userData?.tradingSettings?.tradingHoursStart || "00:00");
const [tradingHoursEnd, setTradingHoursEnd] = useState(userData?.tradingSettings?.tradingHoursEnd || "23:59");
const [minConfidence, setMinConfidence] = useState(userData?.tradingSettings?.minConfidence?.toString() || "80");
const [dailyLossLimit, setDailyLossLimit] = useState(userData?.tradingSettings?.dailyLossLimit?.toString() || "100");
const [dailyProfitTarget, setDailyProfitTarget] = useState(userData?.tradingSettings?.dailyProfitTarget?.toString() || "200");
const [cooldownMinutes, setCooldownMinutes] = useState(userData?.tradingSettings?.cooldownMinutes?.toString() || "5");
const saveSettingsToFirebase = async (
newSettings: any[],
@@ -99,7 +106,11 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
fixedLot: parseFloat(newFixedLot ?? fixedLot) || 0.01,
breakEvenEnabled: newBeEnabled ?? breakEvenEnabled,
tradingHoursStart: newHoursStart ?? tradingHoursStart,
tradingHoursEnd: newHoursEnd ?? tradingHoursEnd
tradingHoursEnd: newHoursEnd ?? tradingHoursEnd,
minConfidence: parseFloat(minConfidence) || 80,
dailyLossLimit: parseFloat(dailyLossLimit) || 100,
dailyProfitTarget: parseFloat(dailyProfitTarget) || 200,
cooldownMinutes: parseInt(cooldownMinutes) || 5
};
try {
@@ -349,8 +360,13 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
{/* LEFT COLUMN: STATUS & BUTTONS */}
<div className="lg:col-span-1 space-y-6">
{/* STATUS CARDS REMOVED */}
{userData?.mtPlatform === 'deriv_api' && (
<>
<AITradingDashboard userData={userData} engineRunning={robotActive} />
<DerivDiagnosticPanel userData={userData} />
<DerivTestPanel userData={userData} />
</>
)}
{/* MAIN BUTTONS */}
<div className="space-y-3 bg-black/40 border border-white/10 p-4 rounded-2xl">
@@ -676,6 +692,54 @@ export const AutoTradingView: React.FC<{ userData?: any, onUpdate?: (data: any)
</div>
</div>
</div>
{/* RISK MANAGEMENT PANEL */}
<h3 className="font-black italic uppercase text-white tracking-widest text-sm mb-6 flex items-center gap-2 border-b border-brand-red/10 pb-4 mt-8 pt-4 border-t relative z-10 w-full shadow-[0_10px_20px_-10px_rgba(255,0,0,0.1)]">
<Shield size={16} className="text-brand-red" /> RISK MANAGEMENT
</h3>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Min Confidence (%)</label>
<input
type="number"
value={minConfidence}
onChange={(e) => setMinConfidence(e.target.value)}
onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-zinc-500 uppercase tracking-widest">Cooldown (mins)</label>
<input
type="number"
value={cooldownMinutes}
onChange={(e) => setCooldownMinutes(e.target.value)}
onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)}
className="w-full bg-black border border-white/10 rounded-lg p-3 text-sm text-white font-mono focus:border-brand-red outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-red-500 uppercase tracking-widest">Daily Loss Limit ($)</label>
<input
type="number"
value={dailyLossLimit}
onChange={(e) => setDailyLossLimit(e.target.value)}
onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)}
className="w-full bg-black border border-red-500/30 rounded-lg p-3 text-sm text-red-400 font-mono focus:border-red-500 outline-none transition-colors"
/>
</div>
<div className="space-y-1">
<label className="text-[10px] font-black text-green-500 uppercase tracking-widest">Daily Profit Target ($)</label>
<input
type="number"
value={dailyProfitTarget}
onChange={(e) => setDailyProfitTarget(e.target.value)}
onBlur={() => saveSettingsToFirebase(tradingSettings, riskPerTrade, maxPositions)}
className="w-full bg-black border border-green-500/30 rounded-lg p-3 text-sm text-green-400 font-mono focus:border-green-500 outline-none transition-colors"
/>
</div>
</div>
</div>
</div>
</div>
+106
View File
@@ -0,0 +1,106 @@
import React, { useEffect, useState } from 'react';
import { Network, Activity, Clock, ShieldCheck, AlertTriangle, Disc } from 'lucide-react';
import { cn } from '../lib/utils';
import axios from 'axios';
export const DerivDiagnosticPanel = ({ userData }: { userData: any }) => {
const [status, setStatus] = useState<'ONLINE' | 'OFFLINE' | 'CONNECTING'>('CONNECTING');
const [account, setAccount] = useState<string>('-------');
const [balance, setBalance] = useState<string>('---');
const [currency, setCurrency] = useState<string>('---');
const [openPositions, setOpenPositions] = useState<number>(0);
const [lastTick, setLastTick] = useState<'OK' | 'WAITING' | 'ERROR'>('WAITING');
const [latency, setLatency] = useState<number>(0);
const checkConnection = async () => {
try {
if (userData?.mtPlatform !== 'deriv_api' || (!userData?.savedDerivToken && !userData?.mtPassword)) {
setStatus('OFFLINE');
return;
}
const token = userData?.savedDerivToken || userData?.mtPassword;
const start = performance.now();
const res = await axios.get('/api/deriv/account', {
headers: { Authorization: `Bearer ${token}` }
});
const end = performance.now();
if (res.data?.success) {
setStatus('ONLINE');
setAccount(res.data.loginid);
setBalance(res.data.balance.toString());
setCurrency(res.data.currency);
setOpenPositions(res.data.openPositions || 0);
setLatency(Math.round(end - start));
setLastTick('OK');
} else {
setStatus('OFFLINE');
}
} catch (e) {
setStatus('OFFLINE');
setLastTick('ERROR');
}
};
useEffect(() => {
checkConnection();
const interval = setInterval(checkConnection, 15000);
return () => clearInterval(interval);
}, [userData]);
if (userData?.mtPlatform !== 'deriv_api') return null;
return (
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-5 space-y-4">
<h3 className="font-black italic uppercase text-white tracking-widest text-sm flex items-center justify-between border-b border-white/5 pb-3">
<div className="flex items-center gap-2">
<Disc size={16} className={cn("transition-colors", status === 'ONLINE' ? 'text-green-500 blur-[1px]' : 'text-red-500 blur-[1px]')} />
DERIV STATUS
</div>
{status === 'ONLINE' ? (
<span className="text-green-500 text-xs px-2 py-0.5 bg-green-500/10 rounded-full border border-green-500/30 font-mono flex items-center gap-1.5 shadow-[0_0_10px_rgba(0,255,0,0.2)]">
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div> ONLINE
</span>
) : (
<span className="text-red-500 text-xs px-2 py-0.5 bg-red-500/10 rounded-full border border-red-500/30 font-mono flex items-center gap-1.5 shadow-[0_0_10px_rgba(255,0,0,0.2)]">
<div className="w-1.5 h-1.5 rounded-full bg-red-500"></div> OFFLINE
</span>
)}
</h3>
<div className="grid grid-cols-2 lg:grid-cols-3 gap-3 text-sm font-mono">
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Account</div>
<div className="text-white flex items-center gap-2">
<ShieldCheck size={14} className="text-brand-red" /> {account}
</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Balance</div>
<div className="text-white flex items-center gap-2">
<Activity size={14} className="text-brand-red" /> {balance} {currency}
</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Open Positions</div>
<div className="text-white flex items-center gap-2">
<Activity size={14} className="text-brand-red" /> {openPositions}
</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Last Tick</div>
<div className={cn("flex items-center gap-2", lastTick === 'OK' ? 'text-green-400' : lastTick === 'ERROR' ? 'text-red-400' : 'text-yellow-400')}>
<Clock size={14} /> {lastTick}
</div>
</div>
<div className="bg-black/50 p-3 rounded-xl border border-white/5">
<div className="text-[9px] text-zinc-500 uppercase tracking-widest mb-1">Latency</div>
<div className="text-white flex items-center gap-2">
<Network size={14} className="text-brand-red" /> {latency}ms
</div>
</div>
</div>
</div>
);
};
+272
View File
@@ -0,0 +1,272 @@
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import { Play, TrendingDown, TrendingUp, RefreshCcw, XCircle, Clock, AlertTriangle, CheckCircle2 } from 'lucide-react';
interface Position {
contract_id: number;
symbol: string;
contract_type: string;
buy_price: number;
currency: string;
// Note: portfolio API doesn't return current profit directly, we might just show buy price for now or mock if needed.
// Actually, Deriv API portfolio returns: contract_id, symbol, contract_type, buy_price, payout, transaction_id...
}
interface LogEntry {
time: Date;
action: string;
result: string;
error?: string;
}
export const DerivTestPanel = ({ userData }: { userData: any }) => {
const [symbol, setSymbol] = useState('R_100'); // Volatility 100 Index as default
const [amount, setAmount] = useState(1);
const [positions, setPositions] = useState<Position[]>([]);
const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(false);
const token = userData?.savedDerivToken || userData?.mtPassword;
const appId = userData?.derivAppId || '1089';
const addLog = (action: string, result: string, error?: string) => {
setLogs(prev => [{ time: new Date(), action, result, error }, ...prev].slice(0, 50));
};
const fetchPositions = async () => {
if (!token) return;
setLoading(true);
try {
const res = await axios.get(`/api/deriv/positions?appId=${appId}`, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.success) {
setPositions(res.data.positions || []);
addLog('REFRESH POSITIONS', `Fetched ${res.data.positions?.length || 0} positions`);
}
} catch (e: any) {
addLog('REFRESH POSITIONS', 'Failed', e.response?.data?.error || e.message);
} finally {
setLoading(false);
}
};
const handleTrade = async (direction: 'BUY' | 'SELL') => {
if (!token) return;
setLoading(true);
const contract_typeBase = direction === 'BUY' ? 'CALL' : 'PUT';
try {
const res = await axios.post(`/api/deriv/test-buy?appId=${appId}`, {
symbol,
amount,
contract_typeBase,
duration: 5,
duration_unit: 't'
}, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.success) {
addLog(`TEST ${direction}`, `Order Executed: ${res.data.trade?.contract_id}`);
fetchPositions();
}
} catch (e: any) {
addLog(`TEST ${direction}`, 'Failed', e.response?.data?.error || e.message);
} finally {
setLoading(false);
}
};
const closePosition = async (contractId: number) => {
if (!token) return;
setLoading(true);
try {
const res = await axios.post(`/api/deriv/close-trade?appId=${appId}`, {
contractId
}, {
headers: { Authorization: `Bearer ${token}` }
});
if (res.data.success) {
addLog('CLOSE POSITION', `Closed contract ${contractId}`);
fetchPositions();
}
} catch (e: any) {
addLog('CLOSE POSITION', `Failed contract ${contractId}`, e.response?.data?.error || e.message);
} finally {
setLoading(false);
}
};
const closeAllPositions = async () => {
if (!token || positions.length === 0) return;
setLoading(true);
let successCount = 0;
for (const pos of positions) {
try {
await axios.post(`/api/deriv/close-trade?appId=${appId}`, {
contractId: pos.contract_id
}, {
headers: { Authorization: `Bearer ${token}` }
});
successCount++;
} catch (e: any) {
addLog('CLOSE POSITION', `Failed contract ${pos.contract_id}`, e.response?.data?.error || e.message);
}
}
addLog('CLOSE ALL', `Closed ${successCount}/${positions.length} positions`);
fetchPositions();
setLoading(false);
};
useEffect(() => {
if (token) {
fetchPositions();
}
}, [token]);
if (userData?.mtPlatform !== 'deriv_api') return null;
return (
<div className="bg-[#0a0000] border border-brand-red/20 rounded-2xl p-5 space-y-6 mt-4">
<h3 className="font-black italic uppercase text-white tracking-widest text-lg border-b border-white/5 pb-3">Trading Test</h3>
{/* Controls */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="space-y-2">
<label className="text-xs text-zinc-500 uppercase tracking-widest">Symbol</label>
<input
type="text"
value={symbol}
onChange={(e) => setSymbol(e.target.value.toUpperCase())}
className="w-full bg-black/50 border border-white/10 rounded-lg px-3 py-2 text-white font-mono text-sm focus:border-brand-red focus:outline-none"
/>
</div>
<div className="space-y-2">
<label className="text-xs text-zinc-500 uppercase tracking-widest">Amount (USD)</label>
<input
type="number"
value={amount}
min={1}
onChange={(e) => setAmount(Number(e.target.value))}
className="w-full bg-black/50 border border-white/10 rounded-lg px-3 py-2 text-white font-mono text-sm focus:border-brand-red focus:outline-none"
/>
</div>
<div className="space-y-2 flex flex-col justify-end">
<button
onClick={() => handleTrade('BUY')}
disabled={loading}
className="w-full bg-green-500/10 hover:bg-green-500/20 text-green-500 border border-green-500/30 rounded-lg px-3 py-2 font-black italic uppercase tracking-widest text-sm flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
>
<TrendingUp size={16} /> Buy Test
</button>
</div>
<div className="space-y-2 flex flex-col justify-end">
<button
onClick={() => handleTrade('SELL')}
disabled={loading}
className="w-full bg-red-500/10 hover:bg-red-500/20 text-red-500 border border-red-500/30 rounded-lg px-3 py-2 font-black italic uppercase tracking-widest text-sm flex items-center justify-center gap-2 transition-colors disabled:opacity-50"
>
<TrendingDown size={16} /> Sell Test
</button>
</div>
</div>
{/* Actions & Tables */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h4 className="text-sm text-zinc-400 font-bold uppercase tracking-widest">Open Positions</h4>
<div className="flex gap-2">
<button onClick={fetchPositions} disabled={loading} className="p-2 hover:bg-white/5 rounded-lg text-zinc-400 hover:text-white transition-colors">
<RefreshCcw size={16} className={loading ? 'animate-spin' : ''} />
</button>
<button onClick={closeAllPositions} disabled={loading || positions.length === 0} className="px-3 py-1.5 bg-brand-red/20 text-brand-red border border-brand-red/30 rounded-lg text-xs font-bold uppercase tracking-widest hover:bg-brand-red/30 transition-colors disabled:opacity-50">
Close All
</button>
</div>
</div>
<div className="bg-black/50 border border-white/5 rounded-xl overflow-hidden">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-white/5 text-zinc-500 text-[10px] uppercase tracking-widest">
<tr>
<th className="px-4 py-3 font-medium">Contract ID</th>
<th className="px-4 py-3 font-medium">Symbol</th>
<th className="px-4 py-3 font-medium">Direction</th>
<th className="px-4 py-3 font-medium">Entry Price</th>
<th className="px-4 py-3 font-medium text-right">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 text-zinc-300">
{positions.length === 0 ? (
<tr>
<td colSpan={5} className="px-4 py-8 text-center text-zinc-500 text-xs uppercase tracking-widest">No open positions</td>
</tr>
) : (
positions.map((pos) => (
<tr key={pos.contract_id} className="hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-3 font-mono text-xs">{pos.contract_id}</td>
<td className="px-4 py-3 font-bold">{pos.symbol}</td>
<td className="px-4 py-3">
<span className={`px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-widest ${pos.contract_type.includes('UP') || pos.contract_type === 'CALL' ? 'bg-green-500/20 text-green-400' : 'bg-red-500/20 text-red-400'}`}>
{pos.contract_type}
</span>
</td>
<td className="px-4 py-3 font-mono">{pos.buy_price} {pos.currency}</td>
<td className="px-4 py-3 text-right">
<button
onClick={() => closePosition(pos.contract_id)}
className="text-brand-red hover:text-red-400 transition-colors p-1"
title="Close Position"
>
<XCircle size={16} />
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
{/* Logs */}
<div className="space-y-4">
<h4 className="text-sm text-zinc-400 font-bold uppercase tracking-widest">Trade Logs</h4>
<div className="bg-black/50 border border-white/5 rounded-xl overflow-hidden max-h-60 overflow-y-auto">
<table className="w-full text-left text-sm whitespace-nowrap">
<thead className="bg-white/5 text-zinc-500 text-[10px] uppercase tracking-widest sticky top-0">
<tr>
<th className="px-4 py-3 font-medium">Time</th>
<th className="px-4 py-3 font-medium">Action</th>
<th className="px-4 py-3 font-medium">Result</th>
<th className="px-4 py-3 font-medium">Error</th>
</tr>
</thead>
<tbody className="divide-y divide-white/5 text-zinc-300">
{logs.length === 0 ? (
<tr>
<td colSpan={4} className="px-4 py-8 text-center text-zinc-500 text-xs uppercase tracking-widest">No logs yet</td>
</tr>
) : (
logs.map((log, i) => (
<tr key={i} className="hover:bg-white/[0.02] transition-colors">
<td className="px-4 py-2 font-mono text-[10px] text-zinc-500 w-32 flex items-center gap-1.5">
<Clock size={12} />
{log.time.toLocaleTimeString()}
</td>
<td className="px-4 py-2 font-bold text-xs"><span className="px-2 py-0.5 bg-white/5 rounded border border-white/10">{log.action}</span></td>
<td className="px-4 py-2 text-xs flex items-center gap-1.5">
{log.error ? <AlertTriangle size={12} className="text-yellow-500" /> : <CheckCircle2 size={12} className="text-green-500" />}
{log.result}
</td>
<td className="px-4 py-2 text-xs text-red-400 w-full truncate max-w-xs" title={log.error}>{log.error || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
</div>
);
};
+314
View File
@@ -0,0 +1,314 @@
import { Request, Response } from 'express';
import { DerivService } from '../services/deriv/index.js';
import { getDerivSymbol } from '../services/marketData.js';
class DerivManager {
private instances: Map<string, DerivService> = new Map();
private initializing: Map<string, Promise<DerivService>> = new Map();
public async getInstance(appId: string, token: string): Promise<DerivService> {
const key = `${appId}:${token}`;
if (this.instances.has(key)) {
return this.instances.get(key)!;
}
if (this.initializing.has(key)) {
return this.initializing.get(key)!;
}
const initPromise = (async () => {
try {
const service = new DerivService(appId, token);
await service.initialize();
this.instances.set(key, service);
return service;
} finally {
this.initializing.delete(key);
}
})();
this.initializing.set(key, initPromise);
return initPromise;
}
public removeInstance(appId: string, token: string) {
const key = `${appId}:${token}`;
if (this.instances.has(key)) {
this.instances.get(key)!.disconnect();
this.instances.delete(key);
}
}
}
export const derivManager = new DerivManager();
export const connect = async (req: Request, res: Response) => {
try {
const { token } = req.body;
const appId = req.body.appId || '1089';
if (!token) return res.status(400).json({ error: 'Token is required' });
console.log(`[Deriv] Connecting with appId: ${appId}...`);
await derivManager.getInstance(appId, token);
res.json({ success: true, message: 'Connected to Deriv' });
} catch (err: any) {
console.error('[Deriv] Connection Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Connection failed' });
}
};
export const getAccount = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
const service = await derivManager.getInstance(appId, token);
const { balance, currency } = await service.account.getBalance();
// Re-authorize to ensure we have the loginid
const authData = await service.auth.authorize();
// Get open positions
let openPositionsCount = 0;
try {
const positions = await service.trading.getOpenPositions();
openPositionsCount = Array.isArray(positions) ? positions.length : 0;
} catch(e) {
console.error('[Deriv] Fetch positions error:', e);
}
res.json({
success: true,
balance,
currency,
loginid: authData?.loginid || '',
openPositions: openPositionsCount
});
} catch (err: any) {
console.error('[Deriv] Get Account Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Failed to fetch account' });
}
};
export const getSymbols = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
const service = await derivManager.getInstance(appId, token);
const response = await service.ws.send({
active_symbols: "brief",
product_type: "basic"
});
res.json({
success: true,
symbols: response.active_symbols
});
} catch (err: any) {
console.error('[Deriv] Get Symbols Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Failed to fetch symbols' });
}
};
export const getTick = async (req: Request, res: Response) => {
let cleanSymbol = 'R_100';
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
const { symbol } = req.params;
cleanSymbol = getDerivSymbol(symbol) || 'R_100';
console.log(`[Deriv] Get Tick called with symbol:`, cleanSymbol);
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
const service = await derivManager.getInstance(appId, token);
const response = await service.ws.send({
ticks_history: cleanSymbol,
end: 'latest',
count: 1,
style: 'ticks'
});
if (response.error) {
return res.status(400).json({ error: response.error.message });
}
res.json({
success: true,
tick: {
symbol: cleanSymbol,
quote: response.history?.prices?.[0] || 0,
epoch: response.history?.times?.[0] || 0
}
});
} catch (err: any) {
console.error('[Deriv] Get Tick Error:', err?.message || String(err), 'symbol:', cleanSymbol);
res.json({ success: false, error: err?.message || 'Failed to fetch tick', symbol: cleanSymbol });
}
};
export const testBuy = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
let { symbol, amount, contract_typeBase, duration, duration_unit, multiplier } = req.body;
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
if (!symbol) return res.status(400).json({ error: 'Symbol is required' });
let cleanSymbol = getDerivSymbol(symbol) || 'R_100';
const service = await derivManager.getInstance(appId, token);
let finalMultiplier = multiplier;
if (contract_typeBase && contract_typeBase.includes('MULT')) {
const allowedMultipliers = await getAllowedMultipliers(service, cleanSymbol);
if (allowedMultipliers && allowedMultipliers.length > 0) {
if (!finalMultiplier || !allowedMultipliers.includes(finalMultiplier)) {
finalMultiplier = allowedMultipliers[0]; // Pick the first valid multiplier
}
} else {
return res.status(400).json({ error: `Multipliers not supported for symbol ${cleanSymbol}` });
}
}
const tradeParams: any = {
symbol: cleanSymbol,
amount: amount || 1, // Minimum demo value usually 1 for USD, depends on currency
contract_typeBase: contract_typeBase || 'CALL'
};
if (finalMultiplier) {
tradeParams.multiplier = finalMultiplier;
} else {
tradeParams.duration = duration || 5;
tradeParams.duration_unit = duration_unit || 't';
}
const result = await service.trading.executeTrade(tradeParams);
res.json({
success: true,
trade: result
});
} catch (err: any) {
console.error('[Deriv] Test Buy Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage, details: err });
}
};
const multipliersCache: Map<string, { values: number[], timestamp: number }> = new Map();
async function getAllowedMultipliers(service: DerivService, symbol: string): Promise<number[] | null> {
const cacheKey = symbol;
const cached = multipliersCache.get(cacheKey);
// Cache for 1 hour
if (cached && Date.now() - cached.timestamp < 3600 * 1000) {
return cached.values;
}
try {
const response = await service.ws.send({ contracts_for: symbol });
if (response.contracts_for && response.contracts_for.available) {
const multiContracts = response.contracts_for.available.filter((c: any) => c.contract_category === 'multiplier');
if (multiContracts.length > 0) {
// Different contracts might have different arrays (e.g., MULTUP vs MULTDOWN), usually they are the same
// We flatten and deduplicate the multiplier_range arrays
const allRanges = multiContracts.map((c: any) => c.multiplier_range || c.multipliers || []).flat();
const uniqueMultipliers = Array.from(new Set(allRanges)).filter(Boolean) as number[];
if (uniqueMultipliers.length > 0) {
uniqueMultipliers.sort((a, b) => a - b);
multipliersCache.set(cacheKey, { values: uniqueMultipliers, timestamp: Date.now() });
return uniqueMultipliers;
}
}
}
} catch (err) {
console.error(`[Deriv] Error fetching allowed multipliers for ${symbol}:`, err);
}
return null;
}
export const getPositions = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
const service = await derivManager.getInstance(appId, token);
const positions = await service.trading.getOpenPositions();
res.json({
success: true,
positions
});
} catch (err: any) {
console.error('[Deriv] Get Positions Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Failed to fetch positions' });
}
};
export const getContractStatus = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
const { contractId } = req.params;
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
if (!contractId) return res.status(400).json({ error: 'Contract ID is required' });
const service = await derivManager.getInstance(appId, token);
const response = await service.ws.send({
proposal_open_contract: 1,
contract_id: Number(contractId)
});
res.json({
success: true,
contract: response.proposal_open_contract
});
} catch (err: any) {
console.error('[Deriv] Get Contract Status Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Failed to fetch contract status' });
}
};
export const closeTrade = async (req: Request, res: Response) => {
try {
const token = (req.headers.authorization || '').replace('Bearer ', '');
const appId = (req.query.appId as string) || '1089';
const { contractId } = req.body;
if (!token) return res.status(401).json({ error: 'Unauthorized: No token provided' });
if (!contractId) return res.status(400).json({ error: 'Contract ID is required' });
const service = await derivManager.getInstance(appId, token);
const result = await service.trading.closeTrade(contractId);
res.json({
success: true,
result
});
} catch (err: any) {
console.error('[Deriv] Close Trade Error:', err);
const errMessage = err?.message || String(err);
res.status(500).json({ error: errMessage || 'Failed to close trade' });
}
};
+15
View File
@@ -0,0 +1,15 @@
import { Router } from 'express';
import * as derivController from '../controllers/deriv.controller.js';
const router = Router();
router.post('/connect', derivController.connect);
router.get('/account', derivController.getAccount);
router.get('/symbols', derivController.getSymbols);
router.get('/tick/:symbol', derivController.getTick);
router.get('/positions', derivController.getPositions);
router.get('/contract/:contractId', derivController.getContractStatus);
router.post('/test-buy', derivController.testBuy);
router.post('/close-trade', derivController.closeTrade);
export default router;
+76
View File
@@ -0,0 +1,76 @@
import axios from 'axios';
export interface AISignal {
symbol: string;
action: 'BUY' | 'SELL';
confidence: number;
reason: string;
timestamp: number;
}
export interface RiskLimits {
maxDailyLoss: number;
maxOpenPositions: number;
maxTradeValue: number;
minConfidence?: number;
dailyProfitTarget?: number;
}
export class AITradingEngine {
static async analyze(symbol: string, currentPrice: number): Promise<AISignal> {
// Simulated AI Analysis
// Em um sistema real, aqui chamaria um endpoint de machine learning ou usaria indicadores técnicos reais
const confidence = Math.floor(Math.random() * 40) + 60; // 60 to 100
const action: 'BUY' | 'SELL' = Math.random() > 0.5 ? 'BUY' : 'SELL';
const reasons = [
'RSI indicates oversold conditions combined with strong volume surge.',
'MACD crossover detected on primary timeframe with volatility expansion.',
'Price action bounced from major institutional support/resistance level.',
'Moving Average ribbon alignment indicates strong trend strength.',
'Institutional order block detected aligning with current momentum.'
];
return {
symbol,
action,
confidence: confidence >= 75 ? confidence + 5 : confidence, // Bias towards higher confidence to trigger some trades
reason: reasons[Math.floor(Math.random() * reasons.length)],
timestamp: Date.now()
};
}
}
export class RiskManager {
static validate(
signal: AISignal,
openPositionsCount: number,
balance: number,
limits: RiskLimits,
tradeAmount: number,
currentDailyProfit: number,
systemOnline: boolean
): { valid: boolean; reason?: string } {
if (!systemOnline) return { valid: false, reason: 'System offline' };
const minConf = limits.minConfidence || 80;
if (signal.confidence < minConf) return { valid: false, reason: `Confidence too low (${signal.confidence}% < ${minConf}%)` };
if (openPositionsCount >= limits.maxOpenPositions) return { valid: false, reason: `Max open positions reached (${openPositionsCount}/${limits.maxOpenPositions})` };
if (tradeAmount > limits.maxTradeValue) return { valid: false, reason: `Trade value exceeds max allowed (${tradeAmount} > ${limits.maxTradeValue})` };
if (balance < tradeAmount) return { valid: false, reason: `Insufficient balance (${balance} < ${tradeAmount})` };
// currentDailyProfit could be negative (loss) or positive (profit)
// Check Loss:
if (currentDailyProfit < 0 && Math.abs(currentDailyProfit) >= limits.maxDailyLoss) {
return { valid: false, reason: `Max daily loss reached (${Math.abs(currentDailyProfit)} >= ${limits.maxDailyLoss})` };
}
// Check Profit Target:
if (limits.dailyProfitTarget && currentDailyProfit >= limits.dailyProfitTarget) {
return { valid: false, reason: `Daily profit target reached (${currentDailyProfit} >= ${limits.dailyProfitTarget})` };
}
return { valid: true };
}
}
+16
View File
@@ -0,0 +1,16 @@
import { DerivWsClient } from './derivWs.js';
export class DerivAccount {
constructor(private client: DerivWsClient) {}
public async getBalance(): Promise<{ balance: number; currency: string }> {
const response = await this.client.send({
balance: 1
});
return {
balance: response.balance.balance,
currency: response.balance.currency
};
}
}
+17
View File
@@ -0,0 +1,17 @@
import { DerivWsClient } from './derivWs.js';
export class DerivAuth {
constructor(private client: DerivWsClient, private token: string) {}
public async authorize(): Promise<any> {
try {
const response = await this.client.send({
authorize: this.token
});
return response.authorize;
} catch (error) {
console.error("Deriv authorization failed:", error);
throw error;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
import { DerivWsClient } from './derivWs.js';
export interface TickResponse {
symbol: string;
quote: number;
epoch: number;
}
export class DerivMarket {
constructor(private client: DerivWsClient) {}
public async subscribeTicks(symbol: string, callback: (tick: TickResponse) => void): Promise<string> {
const response = await this.client.send({
ticks: symbol,
subscribe: 1
});
const subscriptionId = response.subscription?.id;
// Listen for new ticks
this.client.on('tick', (data) => {
if (data.tick && data.tick.symbol === symbol) {
callback({
symbol: data.tick.symbol,
quote: data.tick.quote,
epoch: data.tick.epoch
});
}
});
return subscriptionId;
}
public async unsubscribeTicks(subscriptionId: string): Promise<boolean> {
const response = await this.client.send({
forget: subscriptionId
});
return response.forget === 1;
}
}
+60
View File
@@ -0,0 +1,60 @@
import { DerivWsClient } from './derivWs.js';
export interface ExecuteTradeParams {
symbol: string;
contract_typeBase: 'CALL' | 'PUT' | 'MULTUP' | 'MULTDOWN';
amount: number;
currency?: string;
duration?: number;
duration_unit?: 's' | 'm' | 'h' | 'd' | 't';
basis?: 'stake' | 'payout';
multiplier?: number;
}
export class DerivTrading {
constructor(private client: DerivWsClient) {}
public async executeTrade(params: ExecuteTradeParams): Promise<any> {
const parameters: any = {
amount: params.amount,
basis: params.basis || 'stake',
contract_type: params.contract_typeBase,
currency: params.currency || 'USD',
symbol: params.symbol,
};
if (params.duration && params.duration_unit) {
parameters.duration = params.duration;
parameters.duration_unit = params.duration_unit;
}
if (params.multiplier) {
parameters.multiplier = params.multiplier;
}
const response = await this.client.send({
buy: 1,
price: params.amount,
parameters
});
return response.buy;
}
public async closeTrade(contractId: number): Promise<any> {
const response = await this.client.send({
sell: contractId,
price: 0 // 0 means sell at market price
});
return response.sell;
}
public async getOpenPositions(): Promise<any[]> {
const response = await this.client.send({
portfolio: 1
});
return response.portfolio.contracts;
}
}
+127
View File
@@ -0,0 +1,127 @@
import WebSocket from 'ws';
import { DerivConfig } from './types.js';
export class DerivWsClient {
private ws: WebSocket | null = null;
private config: DerivConfig;
private messageCounter = 1;
private pendingRequests: Map<number, { resolve: (value: any) => void, reject: (reason?: any) => void }> = new Map();
private eventListeners: Map<string, ((data: any) => void)[]> = new Map();
constructor(config: DerivConfig) {
this.config = {
endpoint: 'wss://ws.binaryws.com/websockets/v3',
...config
};
}
private connectionPromise: Promise<void> | null = null;
public onConnect?: () => Promise<void>;
public async connect(): Promise<void> {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
return Promise.resolve();
}
if (this.connectionPromise) {
return this.connectionPromise;
}
this.connectionPromise = new Promise((resolve, reject) => {
const wsUrl = `${this.config.endpoint}?app_id=${this.config.appId}`;
this.ws = new WebSocket(wsUrl);
this.ws.on('open', async () => {
if (this.onConnect) {
try {
await this.onConnect();
resolve();
} catch (e) {
reject(e);
}
} else {
resolve();
}
});
this.ws.on('message', (data: WebSocket.Data) => {
try {
const response = JSON.parse(data.toString());
const reqId = response.req_id;
if (reqId && this.pendingRequests.has(reqId)) {
const { resolve, reject } = this.pendingRequests.get(reqId)!;
this.pendingRequests.delete(reqId);
if (response.error) {
reject(response.error);
} else {
resolve(response);
}
}
// Handle subscriptions
if (response.msg_type) {
const listeners = this.eventListeners.get(response.msg_type) || [];
listeners.forEach(listener => listener(response));
}
} catch (err) {
console.error("Error parsing Deriv WS message:", err);
}
});
this.ws.on('error', (err) => {
this.connectionPromise = null;
reject(err);
});
this.ws.on('close', () => {
this.ws = null;
this.connectionPromise = null;
this.pendingRequests.forEach(({ reject }) => reject(new Error('WebSocket closed')));
this.pendingRequests.clear();
});
});
return this.connectionPromise;
}
public async send(request: any): Promise<any> {
if (!request.authorize) {
await this.connect();
}
return new Promise((resolve, reject) => {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
return reject(new Error('WebSocket is not open'));
}
const reqId = this.messageCounter++;
this.pendingRequests.set(reqId, { resolve, reject });
this.ws.send(JSON.stringify({
...request,
req_id: reqId
}));
});
}
public on(msgType: string, callback: (data: any) => void) {
const listeners = this.eventListeners.get(msgType) || [];
listeners.push(callback);
this.eventListeners.set(msgType, listeners);
}
public off(msgType: string, callback: (data: any) => void) {
const listeners = this.eventListeners.get(msgType) || [];
this.eventListeners.set(msgType, listeners.filter(cb => cb !== callback));
}
public disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
import { DerivWsClient } from './derivWs.js';
import { DerivAuth } from './derivAuth.js';
import { DerivAccount } from './derivAccount.js';
import { DerivMarket } from './derivMarket.js';
import { DerivTrading } from './derivTrading.js';
export class DerivService {
public ws: DerivWsClient;
public auth: DerivAuth;
public account: DerivAccount;
public market: DerivMarket;
public trading: DerivTrading;
constructor(appId: string, token: string) {
this.ws = new DerivWsClient({ appId, token });
this.auth = new DerivAuth(this.ws, token);
this.account = new DerivAccount(this.ws);
this.market = new DerivMarket(this.ws);
this.trading = new DerivTrading(this.ws);
}
public async initialize(): Promise<void> {
await this.ws.connect();
await this.auth.authorize();
// Automatically re-authorize when websocket reconnects
this.ws.onConnect = async () => {
await this.auth.authorize();
};
}
public disconnect(): void {
this.ws.disconnect();
}
}
+14
View File
@@ -0,0 +1,14 @@
export interface DerivConfig {
appId: string;
token: string;
endpoint?: string;
}
export interface DerivResponse<T = any> {
msg_type: string;
error?: {
code: string;
message: string;
};
[key: string]: any;
}
+2
View File
@@ -0,0 +1,2 @@
import axios from 'axios';
console.log('hi');