bingx-monitoring / index.html
EricSam's picture
Update index.html
91283f9 verified
raw
history blame
28.1 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=1024">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nakhoda4X Pro - Portfolio Dashboard</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js"></script>
<script>
// Access environment variables from Hugging Face Static Space
const { BINGX_API_KEY, BINGX_API_SECRET } = window.huggingface.variables || {};
const API_BASE_URL = "https://open-api.bingx.com";
// Generate Signature
function generateSignature(apiSecret, paramsStr) {
return CryptoJS.HmacSHA256(paramsStr, apiSecret).toString(CryptoJS.enc.Hex);
}
// Parse Parameters
function parseParams(params) {
const sortedKeys = Object.keys(params).sort();
const paramPairs = sortedKeys.map(key => `${key}=${params[key]}`);
const paramsStr = paramPairs.join('&');
return paramsStr ? `${paramsStr}&timestamp=${Date.now()}` : `timestamp=${Date.now()}`;
}
// Fetch from BingX API
async function fetchFromAPI(endpoint, params = {}) {
if (!BINGX_API_KEY || !BINGX_API_SECRET) {
throw new Error('BingX API Key and Secret are not set. Please configure them in the Hugging Face Space Secrets.');
}
params.recvWindow = params.recvWindow || 5000;
const paramsStr = parseParams(params);
const signature = generateSignature(BINGX_API_SECRET, paramsStr);
const url = `${API_BASE_URL}${endpoint}?${paramsStr}&signature=${signature}`;
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'X-BX-APIKEY': BINGX_API_KEY,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`API Error ${response.status}: ${errorText}`);
}
return response.json();
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// Fetch Specific Data
async function fetchBalance() {
const data = await fetchFromAPI('/openApi/swap/v3/user/balance');
return data.data.find(b => b.asset === 'USDT')?.walletBalance || 0;
}
async function fetchOpenPositions() {
const data = await fetchFromAPI('/openApi/swap/v2/user/positions', { symbol: 'BTC-USDT' });
return data.data || [];
}
async function fetchTradeHistory() {
const data = await fetchFromAPI('/openApi/swap/v2/user/income', { limit: 100 });
return data.data || [];
}
// Helper Functions
function calculateTodayProfit(trades) {
const today = new Date().toDateString();
return trades.filter(trade => new Date(trade.time).toDateString() === today)
.reduce((sum, trade) => sum + (trade.income || 0), 0);
}
function calculateAdvancedStats(trades) {
let totalProfit = 0, totalLoss = 0, wins = 0;
trades.forEach(trade => {
const pl = trade.income || 0;
if (pl > 0) { totalProfit += pl; wins++; } else { totalLoss += Math.abs(pl); }
});
const profitFactor = totalLoss ? (totalProfit / totalLoss) : 0;
const winRate = trades.length ? (wins / trades.length * 100) : 0;
return { profitFactor: profitFactor.toFixed(2), winRate: winRate.toFixed(1) };
}
function calculatePortfolioAllocation(positions) {
const totalValue = positions.reduce((sum, pos) => sum + (pos.positionValue || 0), 0);
const bySymbol = positions.reduce((acc, pos) => {
acc[pos.symbol] = (acc[pos.symbol] || 0) + (pos.positionValue || 0);
return acc;
}, {});
return { labels: Object.keys(bySymbol), data: Object.values(bySymbol).map(val => totalValue ? (val / totalValue * 100) : 0) };
}
// Update UI Functions
function updateTradingTable(positions, trades) {
const tbody = document.getElementById('trading-table-body');
tbody.innerHTML = '';
positions.forEach(pos => {
tbody.innerHTML += `
<tr class="border-b border-gray-200 dark:border-gray-700">
<td class="py-4">${pos.symbol}</td>
<td class="py-4"><span class="${pos.positionSide === 'LONG' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} px-2 py-1 rounded">${pos.positionSide}</span></td>
<td class="py-4">${pos.quantity}</td>
<td class="py-4">$${pos.entryPrice.toFixed(2)}</td>
<td class="py-4 font-medium">$${pos.markPrice.toFixed(2)}</td>
<td class="py-4 font-bold ${pos.unrealizedProfit > 0 ? 'text-green-500' : 'text-red-500'}">$${pos.unrealizedProfit.toFixed(2)}</td>
<td class="py-4"><span class="px-2 py-1 rounded bg-blue-100 text-blue-800">Open</span></td>
<td class="py-4 text-right"><button class="text-gray-400 hover:text-primary"><i class="fas fa-ellipsis-v"></i></button></td>
</tr>`;
});
trades.slice(0, 5).forEach(trade => {
tbody.innerHTML += `
<tr class="border-b border-gray-200 dark:border-gray-700">
<td class="py-4">${trade.symbol}</td>
<td class="py-4"><span class="${trade.positionSide === 'LONG' ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'} px-2 py-1 rounded">${trade.positionSide}</span></td>
<td class="py-4">${trade.quantity || 0}</td>
<td class="py-4">$${trade.entryPrice?.toFixed(2) || '0.00'}</td>
<td class="py-4 font-medium">$${trade.exitPrice?.toFixed(2) || '0.00'}</td>
<td class="py-4 font-bold ${trade.income > 0 ? 'text-green-500' : 'text-red-500'}">$${trade.income?.toFixed(2) || '0.00'}</td>
<td class="py-4"><span class="px-2 py-1 rounded bg-gray-100 text-gray-800">Closed</span></td>
<td class="py-4 text-right"><button class="text-gray-400 hover:text-primary"><i class="fas fa-ellipsis-v"></i></button></td>
</tr>`;
});
}
function updateAdvancedStats(stats) {
document.getElementById('advanced-stats').innerHTML = `
<div>
<div class="flex justify-between mb-1"><span class="text-gray-500 dark:text-gray-400">Profit Factor</span><span class="font-bold text-green-500">${stats.profitFactor}</span></div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700"><div class="bg-green-600 h-2 rounded-full" style="width: ${Math.min(stats.profitFactor * 25, 100)}%"></div></div>
</div>
<div>
<div class="flex justify-between mb-1"><span class="text-gray-500 dark:text-gray-400">Win Rate</span><span class="font-bold text-purple-500">${stats.winRate}%</span></div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700"><div class="bg-purple-600 h-2 rounded-full" style="width: ${stats.winRate}%"></div></div>
</div>`;
}
function updatePerformanceChart(trades) {
const monthlyPL = trades.reduce((acc, trade) => {
const month = new Date(trade.time).toLocaleString('default', { month: 'short' });
acc[month] = (acc[month] || 0) + (trade.income || 0);
return acc;
}, {});
const performanceChart = new Chart(document.getElementById('performanceChart').getContext('2d'), {
type: 'line',
data: { labels: Object.keys(monthlyPL), datasets: [{ label: 'Profit/Loss', data: Object.values(monthlyPL), borderColor: '#1E90FF', backgroundColor: 'rgba(30, 144, 255, 0.1)', tension: 0.4, fill: true }] },
options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { grid: { color: 'rgba(0, 0, 0, 0.05)', borderDash: [5] }, ticks: { callback: value => '$' + value } }, x: { grid: { display: false } } } }
});
}
function updateAllocationChart(allocation) {
const allocationChart = new Chart(document.getElementById('allocationChart').getContext('2d'), {
type: 'doughnut',
data: { labels: allocation.labels, datasets: [{ data: allocation.data, backgroundColor: ['#1E90FF', '#10b981', '#8b5cf6', '#f59e0b'], borderWidth: 0 }] },
options: { responsive: true, cutout: '65%', plugins: { legend: { display: false }, tooltip: { callbacks: { label: context => `${context.label}: ${context.parsed}%` } } } }
});
const legend = document.getElementById('allocation-legend');
legend.innerHTML = allocation.labels.map((label, i) => `
<div class="mb-3">
<div class="flex justify-between mb-1">
<span class="text-gray-500 dark:text-gray-400 flex items-center">
<span class="h-3 w-3 bg-[${allocationChart.data.datasets[0].backgroundColor[i]}] rounded-full mr-2"></span>
${label}
</span>
<span class="font-medium dark:text-white">${allocation.data[i].toFixed(1)}%</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div class="bg-[${allocationChart.data.datasets[0].backgroundColor[i]}] h-2 rounded-full" style="width: ${allocation.data[i]}%"></div>
</div>
</div>`).join('');
}
// Main Data Fetch Function
async function fetchData() {
try {
document.querySelectorAll('.trading-card h3').forEach(el => el.textContent = 'Loading...');
document.getElementById('trading-table-body').innerHTML = '<tr><td colspan="8" class="py-4 text-center">Loading...</td></tr>';
const [balance, positions, trades] = await Promise.all([
fetchBalance(),
fetchOpenPositions(),
fetchTradeHistory()
]);
document.getElementById('total-balance').textContent = `$${balance.toFixed(2)}`;
document.getElementById('open-trades').textContent = positions.length;
const longCount = positions.filter(p => p.positionSide === 'LONG').length;
document.getElementById('trade-types').innerHTML = `<span class="font-medium">${longCount} Long</span><span class="text-gray-500 mx-2 dark:text-gray-400">β€’</span><span class="font-medium">${positions.length - longCount} Short</span>`;
const todayProfit = calculateTodayProfit(trades);
document.getElementById('today-profit').textContent = `$${todayProfit.toFixed(2)}`;
const riskPercent = balance ? (positions.reduce((sum, p) => sum + (p.positionValue || 0), 0) / balance * 100) : 0;
document.getElementById('risk-exposure').textContent = riskPercent < 20 ? 'Low' : riskPercent < 50 ? 'Medium' : 'High';
document.getElementById('exposure-percent').innerHTML = `<span class="font-medium">${riskPercent.toFixed(1)}%</span><span class="ml-2">of balance</span>`;
updateTradingTable(positions, trades);
const stats = calculateAdvancedStats(trades);
updateAdvancedStats(stats);
updatePerformanceChart(trades);
const allocation = calculatePortfolioAllocation(positions);
updateAllocationChart(allocation);
document.getElementById('last-sync').textContent = `Last synced: ${new Date().toLocaleTimeString()}`;
document.getElementById('allocation-update').textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
} catch (error) {
console.error('Error fetching data:', error);
alert(`Failed to sync with BingX API: ${error.message}. Please check your credentials in Hugging Face Space Secrets, permissions, network, or BingX API documentation (https://bingx-api.github.io/docs/#/en-us/swapV2/account-api.html).`);
}
}
// Event Listeners
document.getElementById('refresh-btn').addEventListener('click', fetchData);
document.getElementById('sync-now').addEventListener('click', fetchData);
window.addEventListener('load', fetchData);
setInterval(fetchData, 120000); // Refresh every 2 minutes, respecting 5/s limit
// Dark Mode Toggle
const themeToggle = document.getElementById('theme-toggle');
themeToggle.addEventListener('click', () => {
document.documentElement.classList.toggle('dark');
});
// Placeholder for Hot Dog Classifier (Static Space limitation)
document.getElementById('hotdog-section').innerHTML = `
<div class="bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 mb-8">
<h3 class="text-lg font-bold text-gray-800 dark:text-white mb-4">Hot Dog Classifier</h3>
<p class="text-gray-500 dark:text-gray-400">This feature is not available in Static Spaces due to server-side model requirements. Use a Gradio Space or convert the model to TensorFlow.js for client-side inference.</p>
<p class="text-gray-500 dark:text-gray-400 mt-2">For TensorFlow.js setup, convert the 'julien-c/hotdog-not-hotdog' model using TensorFlow's model conversion tools and load it with <code>&lt;script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"&gt;&lt;/script&gt;</code>.</p>
</div>
`;
</script>
<style>
.trading-card:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); }
.animate-pulse { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.coin-animation { animation: float 3s ease-in-out infinite; }
@keyframes float { 0% { transform: translateY(0px); } 50% { transform: translateY(-10px); } 100% { transform: translateY(0px); } }
.api-form { max-height: 0; overflow: hidden; transition: max-height 0.3s ease-in-out; }
.api-form.open { max-height: 200px; }
</style>
</head>
<body class="bg-gray-100 dark:bg-darkBg transition-colors duration-300">
<header class="bg-white dark:bg-darkCard shadow-sm">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<div class="flex items-center">
<div class="bg-primary w-10 h-10 rounded-xl flex items-center justify-center mr-3 coin-animation">
<i class="fas fa-chart-line text-white text-xl"></i>
</div>
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Nakhoda4X <span class="text-primary">Pro</span></h1>
</div>
<div class="flex items-center space-x-6">
<button id="theme-toggle" class="text-gray-600 dark:text-gray-300">
<i class="fas fa-moon dark:hidden"></i>
<i class="fas fa-sun hidden dark:block"></i>
</button>
<div class="relative">
<i class="fas fa-bell text-gray-600 dark:text-gray-300 text-xl"></i>
<span class="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full h-5 w-5 flex items-center justify-center">3</span>
</div>
<div class="flex items-center">
<div class="mr-3 text-right">
<p class="font-semibold text-gray-800 dark:text-white">SahabatPipHijau</p>
<p class="text-sm text-gray-500">Premium Account</p>
</div>
<div class="h-10 w-10 rounded-full bg-gradient-to-r from-primary to-secondary flex items-center justify-center text-white font-bold">
J
</div>
</div>
</div>
</div>
</header>
<main class="container mx-auto px-4 py-8">
<div class="flex justify-between items-center mb-8">
<div>
<h2 class="text-3xl font-bold text-gray-800 dark:text-white">Trading Dashboard</h2>
<p class="text-gray-500 mt-1 dark:text-gray-400">Connected to BingX via API</p>
</div>
<div class="flex space-x-4">
<button id="refresh-btn" class="px-4 py-2 bg-white dark:bg-darkCard rounded-lg border border-gray-200 dark:border-gray-700 text-gray-700 dark:text-gray-300 flex items-center">
<i class="fas fa-sync mr-2"></i> Refresh
</button>
<button class="px-4 py-2 bg-primary text-white rounded-lg flex items-center">
<i class="fas fa-plus mr-2"></i> New Trade
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<div class="trading-card bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 transition-all duration-300">
<div class="flex justify-between">
<div>
<p class="text-gray-500 dark:text-gray-400">Total Balance</p>
<h3 id="total-balance" class="text-2xl font-bold mt-2 dark:text-white">$0.00</h3>
</div>
<div class="w-12 h-12 rounded-xl bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<i class="fas fa-wallet text-blue-500 text-xl"></i>
</div>
</div>
<div id="balance-change" class="mt-4 flex items-center text-green-500">
<i class="fas fa-caret-up mr-1"></i>
<span class="font-medium">0.0%</span>
<span class="text-gray-500 ml-2 dark:text-gray-400">last 24h</span>
</div>
</div>
<div class="trading-card bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 transition-all duration-300">
<div class="flex justify-between">
<div>
<p class="text-gray-500 dark:text-gray-400">Open Trades</p>
<h3 id="open-trades" class="text-2xl font-bold mt-2 dark:text-white">0</h3>
</div>
<div class="w-12 h-12 rounded-xl bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<i class="fas fa-exchange-alt text-purple-500 text-xl"></i>
</div>
</div>
<div id="trade-types" class="mt-4 flex items-center text-blue-500">
<span class="font-medium">0 Long</span>
<span class="text-gray-500 mx-2 dark:text-gray-400">β€’</span>
<span class="font-medium">0 Short</span>
</div>
</div>
<div class="trading-card bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 transition-all duration-300">
<div class="flex justify-between">
<div>
<p class="text-gray-500 dark:text-gray-400">Today's Profit</p>
<h3 id="today-profit" class="text-2xl font-bold mt-2 dark:text-white">$0.00</h3>
</div>
<div class="w-12 h-12 rounded-xl bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<i class="fas fa-chart-bar text-green-500 text-xl"></i>
</div>
</div>
<div id="profit-change" class="mt-4 flex items-center text-green-500">
<i class="fas fa-caret-up mr-1"></i>
<span class="font-medium">0.0%</span>
<span class="text-gray-500 ml-2 dark:text-gray-400">vs yesterday</span>
</div>
</div>
<div class="trading-card bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 transition-all duration-300">
<div class="flex justify-between">
<div>
<p class="text-gray-500 dark:text-gray-400">Risk Exposure</p>
<h3 id="risk-exposure" class="text-2xl font-bold mt-2 dark:text-white">Low</h3>
</div>
<div class="w-12 h-12 rounded-xl bg-yellow-100 dark:bg-yellow-900/30 flex items-center justify-center">
<i class="fas fa-exclamation-triangle text-yellow-500 text-xl"></i>
</div>
</div>
<div id="exposure-percent" class="mt-4 flex items-center text-gray-500 dark:text-gray-400">
<span class="font-medium">0%</span>
<span class="ml-2">of balance</span>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
<div class="lg:col-span-2 bg-white dark:bg-darkCard rounded-2xl shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">Monthly Performance</h3>
<div class="flex space-x-3">
<button class="px-3 py-1 rounded-lg text-sm bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white">1M</button>
<button class="px-3 py-1 rounded-lg text-sm bg-primary text-white">3M</button>
<button class="px-3 py-1 rounded-lg text-sm bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white">6M</button>
<button class="px-3 py-1 rounded-lg text-sm bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white">1Y</button>
</div>
</div>
<canvas id="performanceChart"></canvas>
</div>
<div class="bg-white dark:bg-darkCard rounded-2xl shadow-md p-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-white mb-6">Advanced Statistics</h3>
<div id="advanced-stats" class="space-y-5">
<!-- Dynamically populated -->
</div>
</div>
</div>
<div class="bg-white dark:bg-darkCard rounded-2xl shadow-md p-6 mb-8">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">Trading Activity</h3>
<div class="flex space-x-3">
<button class="px-3 py-1 rounded-lg text-sm bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white">All</button>
<button class="px-3 py-1 rounded-lg text-sm bg-primary text-white">Open</button>
<button class="px-3 py-1 rounded-lg text-sm bg-gray-100 dark:bg-gray-800 text-gray-800 dark:text-white">Closed</button>
</div>
</div>
<div class="overflow-x-auto">
<table class="w-full">
<thead>
<tr class="text-left text-gray-500 dark:text-gray-400 text-sm">
<th class="pb-4">Symbol</th>
<th class="pb-4">Type</th>
<th class="pb-4">Size</th>
<th class="pb-4">Entry Price</th>
<th class="pb-4">Current Price</th>
<th class="pb-4">Profit/Loss</th>
<th class="pb-4">Status</th>
<th class="pb-4"></th>
</tr>
</thead>
<tbody id="trading-table-body" class="text-sm">
<!-- Dynamically populated -->
</tbody>
</table>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div class="bg-gradient-to-r from-primary to-secondary rounded-2xl p-6">
<div class="flex items-center mb-4">
<div class="mr-4">
<div class="h-12 w-12 rounded-xl bg-white/30 flex items-center justify-center">
<i class="fas fa-plug text-white text-xl"></i>
</div>
</div>
<div>
<h3 class="text-lg font-bold text-white">BingX API Connected</h3>
<p id="last-sync" class="text-blue-100">Last synced: Just now</p>
</div>
</div>
<button id="toggle-api-form" class="w-full py-2 bg-white rounded-xl text-primary font-bold flex items-center justify-center mb-2">
<i class="fas fa-cog mr-2"></i> Manage API Credentials
</button>
<div id="api-form" class="api-form">
<input id="api-key-input" type="text" placeholder="Enter API Key" class="w-full p-2 mb-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-white" value="${BINGX_API_KEY || ''}" readonly>
<input id="api-secret-input" type="text" placeholder="Enter API Secret" class="w-full p-2 mb-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-800 dark:text-white" value="${BINGX_API_SECRET || ''}" readonly>
<button id="save-api-btn" class="w-full py-2 bg-gray-400 text-white rounded-lg cursor-not-allowed" disabled>Save Credentials (Read-Only)</button>
<p class="text-blue-100 text-sm mt-2">Credentials are managed via Hugging Face Space Secrets.</p>
</div>
<button id="sync-now" class="mt-2 w-full py-3 bg-white rounded-xl text-primary font-bold flex items-center justify-center">
<i class="fas fa-sync mr-2"></i> Sync Now
</button>
</div>
<div class="lg:col-span-2 bg-white dark:bg-darkCard rounded-2xl shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">Portfolio Allocation</h3>
<div>
<span id="allocation-update" class="text-gray-500 dark:text-gray-400 text-sm">Last updated: Just now</span>
</div>
</div>
<div class="flex items-center">
<div class="w-40 h-40 mr-8">
<canvas id="allocationChart"></canvas>
</div>
<div id="allocation-legend" class="flex-grow">
<!-- Dynamically populated -->
</div>
</div>
</div>
</div>
<div id="hotdog-section"></div>
</main>
</body>
</html>