Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Binance Futures Dashboard</title> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/classic.min.css"> | |
| <link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css" rel="stylesheet" /> | |
| <script src="https://cdn.jsdelivr.net/npm/lightweight-charts@3.8.0/dist/lightweight-charts.standalone.production.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"></script> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <style> | |
| .chart-container { | |
| width: 500px; | |
| height: 250px; | |
| margin: 10px; | |
| position: relative; | |
| } | |
| .chart-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(500px, 1fr)); | |
| gap: 10px; | |
| padding: 20px; | |
| } | |
| #sidebar { | |
| width: 300px; | |
| position: fixed; | |
| right: -300px; | |
| top: 0; | |
| height: 100%; | |
| overflow-y: auto; | |
| transition: right 0.5s ease; | |
| background-color: #f8f9fa; | |
| z-index: 1000; | |
| padding: 20px; | |
| box-shadow: -2px 0 5px rgba(0,0,0,0.1); | |
| } | |
| #sidebar.open { | |
| right: 0; | |
| } | |
| #main { | |
| transition: margin-left .5s; | |
| } | |
| .tooltip { | |
| position: absolute; | |
| background-color: rgba(0, 0, 0, 0.7); | |
| color: white; | |
| padding: 5px; | |
| border-radius: 3px; | |
| pointer-events: none; | |
| font-size: 12px; | |
| } | |
| .dark-mode { | |
| background-color: #1a202c; | |
| color: white; | |
| } | |
| .dark-mode .chart-container { | |
| background-color: #2d3748; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <div id="main"> | |
| <header class="bg-white shadow-sm p-4 flex justify-between items-center"> | |
| <h1 class="text-xl font-bold">Binance Futures Dashboard</h1> | |
| <div class="flex space-x-4"> | |
| <select id="timeframe" class="p-2 border rounded"> | |
| <option value="1m">1m</option> | |
| <option value="5m">5m</option> | |
| <option value="15m">15m</option> | |
| <option value="1h">1h</option> | |
| <option value="4h">4h</option> | |
| <option value="1d">1d</option> | |
| <option value="1w">1w</option> | |
| </select> | |
| <button id="settingsBtn" class="p-2 bg-blue-500 text-white rounded">⚙️ Settings</button> | |
| <button id="darkModeToggle" class="p-2 bg-gray-200 rounded">🌙</button> | |
| </div> | |
| </header> | |
| <div class="p-4"> | |
| <div class="mb-2"> | |
| <label for="pairSelector" class="block font-medium mb-1">Select Trading Pairs:</label> | |
| <select id="pairSelector" class="w-full p-2 border rounded" multiple="multiple"></select> | |
| </div> | |
| <div class="flex space-x-2"> | |
| <button id="addPairsBtn" class="flex-1 p-2 bg-green-500 text-white rounded hover:bg-green-600">Add Selected Pairs</button> | |
| <button id="clearPairsBtn" class="flex-1 p-2 bg-red-500 text-white rounded hover:bg-red-600">Clear All</button> | |
| </div> | |
| </div> | |
| <div id="chartGrid" class="chart-grid"></div> | |
| </div> | |
| <div id="sidebar" class="p-4"> | |
| <button id="closeSidebar" class="float-right">✕</button> | |
| <h2 class="text-lg font-bold mb-4">Settings</h2> | |
| <div class="mb-4"> | |
| <h3 class="font-bold mb-2">Display Options</h3> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="showPrice" class="form-checkbox"> | |
| <span>Show Price</span> | |
| </label> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="showVolume" class="form-checkbox"> | |
| <span>Show 24h Volume</span> | |
| </label> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="showOI" class="form-checkbox"> | |
| <span>Show Open Interest</span> | |
| </label> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="showFunding" class="form-checkbox"> | |
| <span>Show Funding Rate</span> | |
| </label> | |
| <label class="flex items-center space-x-2"> | |
| <input type="checkbox" id="showFundingTimer" class="form-checkbox"> | |
| <span>Show Funding Timer</span> | |
| </label> | |
| </div> | |
| <div class="mb-4"> | |
| <h3 class="font-bold mb-2">Chart Style</h3> | |
| <label class="block mb-2"> | |
| <span class="block mb-1">Candle Up Color</span> | |
| <div id="candleUpColor" class="w-full h-8 border"></div> | |
| </label> | |
| <label class="block mb-2"> | |
| <span class="block mb-1">Candle Down Color</span> | |
| <div id="candleDownColor" class="w-full h-8 border"></div> | |
| </label> | |
| </div> | |
| <button id="saveSettings" class="p-2 bg-blue-500 text-white rounded w-full">Save Settings</button> | |
| </div> | |
| <script> | |
| // Global variables | |
| let ws; | |
| const charts = {}; | |
| const subscribedPairs = new Set(); | |
| let colorTheme = { | |
| up: '#26a69a', | |
| down: '#ef5350', | |
| background: '#ffffff', | |
| text: '#333333' | |
| }; | |
| // DOM elements | |
| const pairSelector = $('#pairSelector'); | |
| const chartGrid = document.getElementById('chartGrid'); | |
| const sidebar = document.getElementById('sidebar'); | |
| const settingsBtn = document.getElementById('settingsBtn'); | |
| const closeSidebar = document.getElementById('closeSidebar'); | |
| const darkModeToggle = document.getElementById('darkModeToggle'); | |
| const timeframeSelector = document.getElementById('timeframe'); | |
| // Initialize UI components | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Initialize Select2 for pair selection | |
| $(pairSelector).select2({ | |
| placeholder: "Search and select trading pairs", | |
| allowClear: true, | |
| width: '100%', | |
| closeOnSelect: false | |
| }); | |
| // Load pairs immediately | |
| fetchPairs(); | |
| // Initialize color pickers | |
| const candleUpPicker = Pickr.create({ | |
| el: '#candleUpColor', | |
| theme: 'classic', | |
| default: colorTheme.up, | |
| components: { | |
| preview: true, | |
| opacity: true, | |
| hue: true, | |
| interaction: { | |
| hex: true, | |
| rgba: true, | |
| input: true | |
| } | |
| } | |
| }); | |
| const candleDownPicker = Pickr.create({ | |
| el: '#candleDownColor', | |
| theme: 'classic', | |
| default: colorTheme.down, | |
| components: { | |
| preview: true, | |
| opacity: true, | |
| hue: true, | |
| interaction: { | |
| hex: true, | |
| rgba: true, | |
| input: true | |
| } | |
| } | |
| }); | |
| // Event listeners | |
| document.getElementById('addPairsBtn').addEventListener('click', function() { | |
| console.log('Add pairs clicked'); | |
| addSelectedPairs(); | |
| }); | |
| document.getElementById('clearPairsBtn').addEventListener('click', function() { | |
| console.log('Clear pairs clicked'); | |
| clearAllPairs(); | |
| }); | |
| settingsBtn.addEventListener('click', function() { | |
| console.log('Settings clicked'); | |
| openSidebar(); | |
| }); | |
| closeSidebar.addEventListener('click', function() { | |
| console.log('Close sidebar clicked'); | |
| closeSidebar(); | |
| }); | |
| document.getElementById('saveSettings').addEventListener('click', function() { | |
| console.log('Save settings clicked'); | |
| saveSettings(); | |
| }); | |
| // Close sidebar when clicking outside | |
| document.addEventListener('click', function(event) { | |
| if (!sidebar.contains(event.target) && event.target !== settingsBtn) { | |
| closeSidebar(); | |
| } | |
| }); | |
| // Initialize WebSocket connection | |
| initWebSocket(); | |
| // Load saved settings | |
| loadSettings(); | |
| darkModeToggle.addEventListener('click', function() { | |
| document.body.classList.toggle('dark-mode'); | |
| const isDarkMode = document.body.classList.contains('dark-mode'); | |
| // Update button text | |
| darkModeToggle.textContent = isDarkMode ? '☀️' : '🌙'; | |
| // Update all charts | |
| Object.keys(charts).forEach(pair => { | |
| updateChartTheme(pair, isDarkMode); | |
| }); | |
| // Save dark mode state | |
| const settings = JSON.parse(localStorage.getItem('binanceDashboardSettings') || '{}'); | |
| settings.darkMode = isDarkMode; | |
| localStorage.setItem('binanceDashboardSettings', JSON.stringify(settings)); | |
| }); | |
| document.getElementById('saveSettings').addEventListener('click', saveSettings); | |
| timeframeSelector.addEventListener('change', changeTimeframe); | |
| }); | |
| function initWebSocket() { | |
| ws = new WebSocket('wss://fstream.binance.com/ws'); | |
| ws.onopen = () => { | |
| console.log('WebSocket connected'); | |
| // Resubscribe to any previously subscribed pairs | |
| subscribedPairs.forEach(pair => { | |
| subscribeToPair(pair); | |
| }); | |
| }; | |
| ws.onclose = () => { | |
| console.log('WebSocket disconnected, attempting to reconnect...'); | |
| setTimeout(initWebSocket, 1000); | |
| }; | |
| ws.onerror = (error) => { | |
| console.error('WebSocket error:', error); | |
| }; | |
| ws.onmessage = (event) => { | |
| const message = JSON.parse(event.data); | |
| processWebSocketMessage(message); | |
| }; | |
| } | |
| function fetchPairs() { | |
| console.log('Fetching available pairs...'); | |
| fetch('https://fapi.binance.com/fapi/v1/exchangeInfo') | |
| .then(response => response.json()) | |
| .then(data => { | |
| const pairs = data.symbols | |
| .filter(symbol => symbol.status === 'TRADING') | |
| .map(symbol => ({ | |
| id: symbol.symbol, | |
| text: symbol.symbol | |
| })); | |
| console.log(`Loaded ${pairs.length} trading pairs`); | |
| $(pairSelector).select2({ | |
| data: pairs, | |
| placeholder: "Search and select trading pairs", | |
| allowClear: true, | |
| width: '100%', | |
| closeOnSelect: false | |
| }); | |
| }) | |
| .catch(error => { | |
| console.error('Error fetching pairs:', error); | |
| // Retry after 5 seconds if failed | |
| setTimeout(fetchPairs, 5000); | |
| }); | |
| } | |
| function addSelectedPairs() { | |
| const selectedPairs = pairSelector.val(); | |
| if (!selectedPairs) return; | |
| selectedPairs.forEach(pair => { | |
| if (!subscribedPairs.has(pair)) { | |
| subscribedPairs.add(pair); | |
| subscribeToPair(pair); | |
| createChart(pair); | |
| } | |
| }); | |
| // Save selected pairs | |
| const settings = JSON.parse(localStorage.getItem('binanceDashboardSettings') || '{}'); | |
| settings.selectedPairs = Array.from(subscribedPairs); | |
| localStorage.setItem('binanceDashboardSettings', JSON.stringify(settings)); | |
| } | |
| function clearAllPairs() { | |
| subscribedPairs.forEach(pair => { | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| const unsubscribe = { | |
| method: "UNSUBSCRIBE", | |
| params: [ | |
| `${pair.toLowerCase()}@kline_${timeframeSelector.value}`, | |
| `${pair.toLowerCase()}@markPrice@1s`, | |
| `${pair.toLowerCase()}@bookTicker` | |
| ], | |
| id: Date.now() | |
| }; | |
| ws.send(JSON.stringify(unsubscribe)); | |
| } | |
| const chartElement = document.getElementById(`chart-${pair}`); | |
| if (chartElement) { | |
| chartElement.remove(); | |
| } | |
| }); | |
| subscribedPairs.clear(); | |
| Object.keys(charts).forEach(pair => delete charts[pair]); | |
| // Clear saved pairs | |
| const settings = JSON.parse(localStorage.getItem('binanceDashboardSettings') || '{}'); | |
| settings.selectedPairs = []; | |
| localStorage.setItem('binanceDashboardSettings', JSON.stringify(settings)); | |
| } | |
| function subscribeToPair(pair) { | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| const timeframe = timeframeSelector.value; | |
| const subscription = { | |
| method: "SUBSCRIBE", | |
| params: [ | |
| `${pair.toLowerCase()}@kline_${timeframe}`, | |
| `${pair.toLowerCase()}@markPrice@1s`, | |
| `${pair.toLowerCase()}@bookTicker` | |
| ], | |
| id: Date.now() | |
| }; | |
| ws.send(JSON.stringify(subscription)); | |
| } | |
| } | |
| function createChart(pair) { | |
| const container = document.createElement('div'); | |
| container.className = 'chart-container'; | |
| container.id = `chart-${pair}`; | |
| const title = document.createElement('div'); | |
| title.className = 'font-bold mb-2'; | |
| title.id = `title-${pair}`; | |
| title.textContent = pair; | |
| const chartElement = document.createElement('div'); | |
| chartElement.style.width = '100%'; | |
| chartElement.style.height = '100%'; | |
| container.appendChild(title); | |
| container.appendChild(chartElement); | |
| chartGrid.appendChild(container); | |
| const chart = LightweightCharts.createChart(chartElement, { | |
| width: 500, | |
| height: 250, | |
| layout: { | |
| backgroundColor: colorTheme.background, | |
| textColor: colorTheme.text | |
| }, | |
| grid: { | |
| vertLines: { color: '#eee' }, | |
| horzLines: { color: '#eee' } | |
| }, | |
| crosshair: { | |
| mode: LightweightCharts.CrosshairMode.Normal | |
| }, | |
| timeScale: { | |
| timeVisible: true, | |
| secondsVisible: false | |
| } | |
| }); | |
| const candleSeries = chart.addCandlestickSeries({ | |
| upColor: colorTheme.up, | |
| downColor: colorTheme.down, | |
| borderVisible: false, | |
| wickUpColor: colorTheme.up, | |
| wickDownColor: colorTheme.down | |
| }); | |
| const volumeSeries = chart.addHistogramSeries({ | |
| color: '#26a69a', | |
| priceFormat: { | |
| type: 'volume' | |
| }, | |
| priceScaleId: '', | |
| scaleMargins: { | |
| top: 0.8, | |
| bottom: 0 | |
| } | |
| }); | |
| charts[pair] = { | |
| chart, | |
| candleSeries, | |
| volumeSeries, | |
| lastUpdate: null, | |
| data: [] | |
| }; | |
| // Fetch initial historical data | |
| fetchInitialData(pair); | |
| } | |
| function fetchInitialData(pair) { | |
| const timeframe = timeframeSelector.value; | |
| const limit = 100; | |
| fetch(`https://fapi.binance.com/fapi/v1/klines?symbol=${pair}&interval=${timeframe}&limit=${limit}`) | |
| .then(response => response.json()) | |
| .then(data => { | |
| const formattedData = data.map(item => ({ | |
| time: item[0] / 1000, | |
| open: parseFloat(item[1]), | |
| high: parseFloat(item[2]), | |
| low: parseFloat(item[3]), | |
| close: parseFloat(item[4]), | |
| volume: parseFloat(item[5]) | |
| })); | |
| const volumes = data.map(item => ({ | |
| time: item[0] / 1000, | |
| value: parseFloat(item[5]), | |
| color: parseFloat(item[4]) >= parseFloat(item[1]) ? colorTheme.up : colorTheme.down | |
| })); | |
| if (charts[pair]) { | |
| charts[pair].candleSeries.setData(formattedData); | |
| charts[pair].volumeSeries.setData(volumes); | |
| charts[pair].data = formattedData; | |
| } | |
| }) | |
| .catch(error => console.error(`Error fetching initial data for ${pair}:`, error)); | |
| } | |
| function processWebSocketMessage(message) { | |
| if (message.e === 'kline') { | |
| const pair = message.s; | |
| const kline = message.k; | |
| if (charts[pair]) { | |
| const newCandle = { | |
| time: kline.t / 1000, | |
| open: parseFloat(kline.o), | |
| high: parseFloat(kline.h), | |
| low: parseFloat(kline.l), | |
| close: parseFloat(kline.c), | |
| volume: parseFloat(kline.v) | |
| }; | |
| // Update or add candle | |
| if (kline.x) { | |
| // Closed candle | |
| charts[pair].data.push(newCandle); | |
| if (charts[pair].data.length > 100) { | |
| charts[pair].data.shift(); | |
| } | |
| charts[pair].candleSeries.update(newCandle); | |
| } else { | |
| // Update current candle | |
| charts[pair].candleSeries.update(newCandle); | |
| } | |
| // Update volume | |
| const newVolume = { | |
| time: kline.t / 1000, | |
| value: parseFloat(kline.v), | |
| color: parseFloat(kline.c) >= parseFloat(kline.o) ? colorTheme.up : colorTheme.down | |
| }; | |
| charts[pair].volumeSeries.update(newVolume); | |
| } | |
| } | |
| // Handle other message types (mark price, book ticker, etc.) | |
| } | |
| function changeTimeframe() { | |
| const timeframe = timeframeSelector.value; | |
| subscribedPairs.forEach(pair => { | |
| // Unsubscribe from old streams | |
| if (ws && ws.readyState === WebSocket.OPEN) { | |
| const unsubscribe = { | |
| method: "UNSUBSCRIBE", | |
| params: [ | |
| `${pair.toLowerCase()}@kline_${timeframe}`, | |
| `${pair.toLowerCase()}@markPrice@1s`, | |
| `${pair.toLowerCase()}@bookTicker` | |
| ], | |
| id: Date.now() | |
| }; | |
| ws.send(JSON.stringify(unsubscribe)); | |
| } | |
| // Subscribe to new streams | |
| subscribeToPair(pair); | |
| // Refresh chart data | |
| if (charts[pair]) { | |
| fetchInitialData(pair); | |
| } | |
| }); | |
| } | |
| function openSidebar() { | |
| sidebar.classList.add('open'); | |
| } | |
| function closeSidebar() { | |
| sidebar.classList.remove('open'); | |
| } | |
| function updateChartTheme(pair, isDarkMode) { | |
| charts[pair].chart.applyOptions({ | |
| layout: { | |
| backgroundColor: isDarkMode ? '#2d3748' : '#ffffff', | |
| textColor: isDarkMode ? '#ffffff' : '#333333' | |
| }, | |
| grid: { | |
| vertLines: { color: isDarkMode ? '#4a5568' : '#eee' }, | |
| horzLines: { color: isDarkMode ? '#4a5568' : '#eee' } | |
| } | |
| }); | |
| } | |
| function saveSettings() { | |
| try { | |
| const settings = { | |
| showPrice: document.getElementById('showPrice').checked, | |
| showVolume: document.getElementById('showVolume').checked, | |
| showOI: document.getElementById('showOI').checked, | |
| showFunding: document.getElementById('showFunding').checked, | |
| showFundingTimer: document.getElementById('showFundingTimer').checked, | |
| darkMode: document.body.classList.contains('dark-mode'), | |
| selectedPairs: Array.from(subscribedPairs), | |
| timeframe: document.getElementById('timeframe').value | |
| }; | |
| localStorage.setItem('binanceDashboardSettings', JSON.stringify(settings)); | |
| console.log('Settings saved successfully'); | |
| closeSidebar(); | |
| } catch (error) { | |
| console.error('Error saving settings:', error); | |
| } | |
| } | |
| function loadSettings() { | |
| const savedSettings = localStorage.getItem('binanceDashboardSettings'); | |
| if (savedSettings) { | |
| const settings = JSON.parse(savedSettings); | |
| document.getElementById('showPrice').checked = settings.showPrice; | |
| document.getElementById('showVolume').checked = settings.showVolume; | |
| document.getElementById('showOI').checked = settings.showOI; | |
| document.getElementById('showFunding').checked = settings.showFunding; | |
| document.getElementById('showFundingTimer').checked = settings.showFundingTimer; | |
| if (settings.darkMode) { | |
| document.body.classList.add('dark-mode'); | |
| } | |
| if (settings.selectedPairs && settings.selectedPairs.length > 0) { | |
| settings.selectedPairs.forEach(pair => { | |
| subscribedPairs.add(pair); | |
| createChart(pair); | |
| }); | |
| } | |
| if (settings.timeframe) { | |
| timeframeSelector.value = settings.timeframe; | |
| } | |
| } | |
| } | |
| </script> | |
| </body> | |
| </html> |