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:
+7
-2
@@ -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
|
||||
|
||||
Generated
+16
-4
@@ -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
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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' });
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
import axios from 'axios';
|
||||
console.log('hi');
|
||||
Reference in New Issue
Block a user